From 1f263494360de0264a0ba55e3d7b7c1245bd062d Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 20:19:57 +0800 Subject: [PATCH] docs: clarify developer workspace editing and request presentation --- src/shell/developer_tools.rs | 28 ++++++++++++++++++++++++++++ src/shell/developer_tools/painter.rs | 5 +++++ 2 files changed, 33 insertions(+) diff --git a/src/shell/developer_tools.rs b/src/shell/developer_tools.rs index b64c4dd..195b9dc 100644 --- a/src/shell/developer_tools.rs +++ b/src/shell/developer_tools.rs @@ -3,6 +3,10 @@ //! 输入法预编辑、光标、选择和控件捕获分别处理,避免把界面操作直接当成业务模型写入。 //! //! Native developer workspace. Coordinates and scroll offsets are logical pixels. +//! +//! shell::app 把命名编辑键、文本/IME 与指针交给当前 Workspace 槽,scene 调用 build 生成下一帧控件和命中布局。 +//! activate 只在控件释放确认或文本框按下后消费 Action;build/Painter 只登记 Action,不在绘制时提交 Console 输入。 +//! submit_console/initialize_tools 的后台线程只做本地 HTTP 传输;VM 执行结果由 API 活动日志展示,不以线程启动或读完响应判定成功。 use crate::{ activity::{Activity, Exchange}, app::AppEvent, @@ -279,14 +283,17 @@ impl DeveloperToolsLayout { target.rect.y += dy; } for (rect, _) in &mut self.splits { + // 分隔条的额外布局与 Widget 目标同时移动,后续 press 才能在平移后的窗口位置找到拖动类别。 rect.x += dx; rect.y += dy; } for (rect, _, _, _) in &mut self.regions { + // 滚轮用区域外框定位内容;只平移外框,不改 Region 身份和完整内容宽高。 rect.x += dx; rect.y += dy; } for (rect, _, _, _) in &mut self.thumbs { + // 手柄几何随窗口平移,已计算的轴方向/内容位移倍率保持不变,不能把平移量当滚动增量。 rect.x += dx; rect.y += dy; } @@ -338,6 +345,7 @@ impl DeveloperToolsState { .as_ref() .is_some_and(|(area, _, _)| area.contains(self.pointer)) { + // 只读源码选择区先于普通 Widget 光标,显示 I-beam 并不赋予源码可编辑或可执行的能力。 return CursorIcon::Text; } // 宿主抽象 Cursor 转换成 winit CursorIcon,保留禁用、抓取与横纵调整等语义。 @@ -500,6 +508,7 @@ impl DeveloperToolsState { Some(Action::Request(id)) => { self.selected_request = Some(id); if self.tab == Tab::Console { + // 点击 Console 的请求标题转到该状态的 Network 视图,清筛选/水位让目标记录可见;不重新发送请求。 self.tab = Tab::Network; self.network_after = 0; self.filter.clear(); @@ -522,6 +531,7 @@ impl DeveloperToolsState { pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) { self.pointer = p; self.interaction.pointer_moved(p, &layout.widgets); + // release 只为原先捕获且仍启用的同目标返回 Action,不能按当前鼠标位置随意激活另一个按钮。 let action = self.interaction.release(&layout.widgets); self.drag = None; self.source_selection.dragging = false; @@ -566,6 +576,7 @@ impl DeveloperToolsState { _ => None, }); if self.focus == Some(Focus::Input) { + // 键盘导航进入 Console 编辑器时将插入点放到当前草稿末尾,再由下一帧测量并滚到可见范围。 self.cursor = self.input.len(); self.reveal_cursor(); } @@ -637,11 +648,13 @@ impl DeveloperToolsState { Some(Drag::Scroll(r, h, ratio)) => { let s = &mut self.scroll[r as usize]; if h { + // 捕获时保存的 h 决定只改横向偏移,ratio 将手柄逻辑位移转换成内容位移。 s.x = (s.x + delta.x * ratio).max(0.0); } else { s.y = (s.y + delta.y * ratio).max(0.0); } if r == Region::Transcript { + // 用户主动拖动输出区后停用 Follow,否则下一帧会立即把刚选择的阅读位置拉回底部。 self.follow = false; } } @@ -658,10 +671,12 @@ impl DeveloperToolsState { .rev() .find(|(area, _, _, _)| area.contains(p)) { + // r 来自本帧命中区域,选择该工具的独立滚动槽;不改变其他面板或其他 Workspace 页的偏移。 let s = &mut self.scroll[*r as usize]; s.x = (s.x - dx).max(0.0); s.y = (s.y - dy).max(0.0); if *r == Region::Transcript { + // 滚轮与手柄拖动采用同样的 Follow 取消规则,下一轮 Painter 再钳最新内容上界。 self.follow = false; } } @@ -728,6 +743,7 @@ impl DeveloperToolsState { // cursor 由编辑路径保持在有效字节边界,当前行前缀测量用于候选窗跟随,不按字符数估算 x。 let before = &self.input[..self.cursor]; let row = before.matches('\n').count() as f32; + // col 只是当前行在光标之前的文本,按真实字形宽度测量;area 给候选窗位置的可见钳制范围。 let col = before.rsplit('\n').next().unwrap_or(""); let area = input.viewport; return Point::new( @@ -747,6 +763,7 @@ impl DeveloperToolsState { return self.source_selection.text(self.selection); } if !self.selection { + // 非源码文本框目前只有全选状态,无选择就返回空串,复制调用方据此保留系统现有剪贴板。 return String::new(); } match self.focus { @@ -796,6 +813,7 @@ impl DeveloperToolsState { match self.focus { Some(Focus::Filter) => { if self.selection { + // Filter 全选时删整框,否则下方 pop 删除末尾 Unicode 标量;这里没有独立字节光标。 self.filter.clear(); } else { self.filter.pop(); @@ -803,6 +821,7 @@ impl DeveloperToolsState { } Some(Focus::Custom) => { if self.selection { + // 颜色编辑框遵循同样的全选删除规则,删除文本本身不会立即应用或重置强调色。 self.custom.clear(); } else { self.custom.pop(); @@ -827,6 +846,7 @@ impl DeveloperToolsState { self.cursor = 0; self.selection = false; } else if *key == Backspace && self.cursor > 0 { + // 光标已在有效字节边界,向前查最后一个完整字素的起点,再删除到当前 cursor 的整个区间。 let n = self.input[..self.cursor] .grapheme_indices(true) .next_back() @@ -835,6 +855,7 @@ impl DeveloperToolsState { self.input.replace_range(n..self.cursor, ""); self.cursor = n; } else if *key == Delete && self.cursor < self.input.len() { + // 后缀非空才取第一个字素长度,删除精确字节区间而不移动当前插入点。 let len = self.input[self.cursor..] .graphemes(true) .next() @@ -969,6 +990,7 @@ impl DeveloperToolsState { source.len(), source )?; + // 响应只用于读完连接,不在此解析状态码/JSON;HTTP 422 等应用错误仍由工具活动记录显示。 let mut response = String::new(); stream.read_to_string(&mut response)?; Ok(()) @@ -977,6 +999,7 @@ impl DeveloperToolsState { if let Err(error) = send() && let Some(proxy) = proxy { + // 仅传输失败走原生错误通知,source 为空表示初始化工具没有可恢复的用户输入草稿。 let _ = proxy.send_event(AppEvent::ConsoleFailed { message: error.to_string(), source: String::new(), @@ -1008,12 +1031,16 @@ impl DeveloperToolsState { // 下方紧凑闭包只负责 HTTP 传输:2 秒连接、120 秒读取和 10 秒写入超时,响应正文不在客户端解析。 let spawned=thread::Builder::new().name("tessera-console-client".into()).spawn(move||{ let send=||->std::io::Result<()> { + // stream 是本次后台请求独占的 TCP 连接,固定本机端点;不持 Workspace/Namespace 锁,也不在 GUI 线程等待。 let mut stream=TcpStream::connect_timeout(&"127.0.0.1:4737".parse().unwrap(),Duration::from_secs(2))?; stream.set_read_timeout(Some(Duration::from_secs(120)))?;stream.set_write_timeout(Some(Duration::from_secs(10)))?; + // reset 发 DELETE 且正文为空,普通输入发 POST;Content-Length 使用 UTF-8 字节数,文本只作为正文传一次。 let method=if reset{"DELETE"}else{"POST"}; write!(stream,"{method} /v1/namespaces/app.main/repl HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nX-Tessera-Origin: console\r\nContent-Length: {}\r\n\r\n{}",source.len(),source)?; + // read_to_string 只等待响应流结束,不把 HTTP 状态或 value/rendered 字段解释成客户端执行成功。 let mut response=String::new();stream.read_to_string(&mut response)?;Ok(()) }; + // 传输错误才带原 source 回到原生线程;app 仅在编辑框仍为空时恢复,避免覆盖用户后来输入的草稿。 if let Err(error)=send()&&let Some(proxy)=proxy{let _=proxy.send_event(AppEvent::ConsoleFailed{message:error.to_string(),source});} }); // 按线程创建结果更新 GUI 状态,不等待网络或 VM 执行;失败创建线程时保留输入并显示错误。 @@ -1057,6 +1084,7 @@ pub fn build( // 布局和命中统一用逻辑像素,最后仅显示列表按设备 scale 转换,返回 layout 仍为逻辑坐标。 let width = width as f32 / scale; let height = height as f32 / scale; + // w/x 只是本帧工具局部画布的宽度和起点别名,侧栏偏移由 scene 外层平移,不在每个子工具里重复扣除。 let w = width; let x = 0.0; let panel = Rect::new(x, 0.0, w, height); diff --git a/src/shell/developer_tools/painter.rs b/src/shell/developer_tools/painter.rs index 1f63ec0..5ca6243 100644 --- a/src/shell/developer_tools/painter.rs +++ b/src/shell/developer_tools/painter.rs @@ -1,6 +1,8 @@ //! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。 //! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。 //! +//! developer_tools::build 为当前工具构造 Painter,各子工具提交 Line/Action;本模块不访问 VM,也不再次执行活动日志里的请求。 +//! lines 的完整文本用于宽度缓存和复制,可见切片用于绘制;行的可见命中矩形与完整背景矩形分开,滚动条在内容裁剪弹出后登记。 use super::*; /// 开发工具的绘制助手,把布局结果写入同帧显示列表。 @@ -123,6 +125,7 @@ impl Painter { if let Some(divider) = line.divider { self.fill( Rect::new(area.x, y, area.width, 1.0), + // 分隔线只选择配色,不创建额外可激活行;Line.action 才决定该行能否登记点击目标。 match divider { Divider::Accent => self.ui.theme.accent, Divider::Neutral => BORDER, @@ -299,6 +302,7 @@ pub(super) fn console_result(out: &mut Vec, body: &str, color: Color) { if let Some(diagnostics) = value["diagnostics"].as_array() && !diagnostics.is_empty() { + // 只解释响应中已有诊断数据,不向 VM 请求 traceback;列表缺失/为空则继续尝试 error 或 revision。 // 每条诊断可选显示位置,再显示 message;缺 message 用固定失败提示,不调用源对象格式化。 for d in diagnostics { if let Some(line) = d["line"].as_u64() { @@ -371,6 +375,7 @@ pub(super) fn request_details(out: &mut Vec, r: &Exchange) { out.push(l); multiline( out, + // 空预览明确显示占位,正文仍来自已记录的受限 Exchange,不在详情展开时重新读网络流。 if body.is_empty() { "(empty)" } else { body }, if title == "RESPONSE BODY" && r.status.is_some_and(|s| s >= 400) { ERROR