diff --git a/src/shell/developer_tools/console.rs b/src/shell/developer_tools/console.rs index a9df118..97b17d6 100644 --- a/src/shell/developer_tools/console.rs +++ b/src/shell/developer_tools/console.rs @@ -4,18 +4,21 @@ use super::*; /// 将有界 HTTP 活动、当前编辑输入和输入法预编辑投影为 Console 显示行与命中布局,并按需滚动到光标。 +// developer_tools::build 在 Console 标签选中时调用;records 是父入口克隆的活动快照,后续绘制不持活动日志锁。 pub(super) fn build( p: &mut Painter, state: &mut DeveloperToolsState, records: &[Exchange], viewport: Rect, ) { + // x 是工具画布横向原点,w 为可用宽度;本页标题及行区纵坐标沿用父工具布局约定。 let x = viewport.x; let w = viewport.width; // 此工具页使用其既定标题栏/底栏布局坐标,输出区与输入区共享下方 transcript 滚动面板。 let bottom = viewport.height - 22.0; p.fill(Rect::new(x, 32.0, w, 30.0), BAR); + // Clear 的可用性与下面输出记录过滤条件一致,仅存在清屏水位之后的可展示记录才启用。 p.enabled_button( Rect::new(x + 3.0, 32.0, 62.0, 29.0), "Clear", @@ -28,6 +31,7 @@ pub(super) fn build( ); p.button( Rect::new(x + 68.0, 32.0, 106.0, 29.0), + // 按钮文本反映当前跟随设置,点击 Action::Follow 后由状态处理器切换,不在绘制时直接改 follow。 if state.follow { "Follow: on" } else { @@ -42,6 +46,7 @@ pub(super) fn build( Action::ResetSession, false, ); + // 窄窗口省略辅助会话说明,保留清屏、跟随和重置按钮,不改变真实会话状态。 if w > 520.0 { p.label(x + 309.0, 39.0, "app.main · live objects", MUTED, false); } @@ -53,7 +58,9 @@ pub(super) fn build( && (r.method == "PUT" || r.method == "POST" || r.method == "DELETE") && !r.path.ends_with("/events") }) { + // 按路径及方法给观察记录选择显示标签,分类不执行 API,也不决定 worker 的真实请求处理。 let kind = if r.path.ends_with("/repl") { + // 同一 /repl 的 DELETE 显示会话重置,其他方法显示持续会话执行,区别于独立 /evaluate。 if r.method == "DELETE" { "RESET SESSION" } else { @@ -67,12 +74,15 @@ pub(super) fn build( "CLEAR DOCUMENT" }; // Console 自己发起的 REPL 请求省略冗余标题和 HTTP 状态行,外部请求保留来源与诊断入口。 + // 只简化本工具发出的 POST /repl;重置和外部请求仍保留来源与状态,避免混淆不同操作。 let local = r.origin == "Console" && r.path.ends_with("/repl") && r.method == "POST"; + // 非本地简化输出加可点击标题,Action::Request 根据稳定请求 ID 打开 Network 详情。 if !local { let mut title = Line::new( format!("#{} {} {} {}", r.id, r.timestamp, r.origin, kind), MUTED, ); + // 强调分隔线标记一次执行起点,标题动作与文本一起交给 Painter 注册命中目标。 title.divider = Some(Divider::Accent); title.action = Some(Action::Request(r.id)); lines.push(title); @@ -84,6 +94,7 @@ pub(super) fn build( TEXT, )); } + // 颜色只按已记录 HTTP 状态判定,pending 尚无失败码时暂用成功色变量,但实际分支显示灰色 Executing。 let color = if r.status.is_some_and(|s| s >= 400) { ERROR } else { @@ -97,14 +108,17 @@ pub(super) fn build( color, )); } + // 响应格式化由 painter 模块解析已记录正文;预览截断导致 JSON 无法解析时会回退原文显示。 console_result(&mut lines, &r.response, color); } else { lines.push(Line::new(" Executing…", MUTED)); } + // 每条请求结束添加中性分隔线,包括仍执行中的记录,让后续输入与相邻历史有明确边界。 let mut separator = Line::new("", MUTED); separator.divider = Some(Divider::Neutral); lines.push(separator); } + // 没有可展示历史时补欢迎说明,这些行也属于 transcript 输出,后面的输入起始偏移包含它们。 if lines.is_empty() { multiline( &mut lines, @@ -115,37 +129,47 @@ pub(super) fn build( use std::hash::{Hash, Hasher}; // 只对输出行文本求 stamp,用来判断 Follow 是否需要跳到底部;当前编辑输入不参与输出变化检测。 let mut hash = std::collections::hash_map::DefaultHasher::new(); + // 只散列 value,不把选择、颜色或分隔线样式变化当成新输出,避免无关重绘触发跟随到底部。 for line in &lines { line.value.hash(&mut hash); } let stamp = hash.finish(); + // 保存本次指纹作下一帧比较,Follow 开关只决定是否采用变化信号,不影响指纹更新。 let output_changed = stamp != state.console_stamp; state.console_stamp = stamp; // 记录输入开始的行号,后续光标和点击区域都需加上 transcript 输出行偏移。 let first_input = lines.len(); + // 输出历史与可编辑输入共用一个 Transcript 区域,底部留出状态提示和滚动条空间。 let area = Rect::new(x + 4.0, 63.0, w - 5.0, (bottom - 88.0).max(1.0)); // 输入法 preedit 只插入显示副本,不提交到实际 input;只有输入框持焦点时展示组合文本。 let mut input = state.input.clone(); + // 只在输入框持焦点时把 preedit 插到显示副本,编辑字符串本身仍等最终 IME Commit。 if state.focus == Some(Focus::Input) { input.insert_str(state.cursor, &state.preedit); } + // 显示输入保留换行并加提示符;全选高亮只跟随 Input 焦点,不把历史输出当成可替换输入。 for (i, line) in input.split('\n').enumerate() { let mut line = Line::new(format!("{}{line}", if i == 0 { "> " } else { " " }), TEXT); line.selected = state.selection && state.focus == Some(Focus::Input); lines.push(line); } + // 提示符用真实等宽字体度量,后续光标和鼠标定位共享同一文本起点。 let prefix = text::measure("> ", TextStyle::code()).width; // cursor 是输入字符串的字节边界,由编辑逻辑维护;通过换行计数定位行,再用真实字体宽度计算列位置。 let before = &state.input[..state.cursor]; + // cursor 以原输入的 UTF-8 字节位置计,按其之前换行数求行号,最后一段就是当前行已有正文。 let row = before.matches('\n').count() as f32; let column = before.rsplit('\n').next().unwrap_or(""); + // 列正文与 preedit 一起测量,光标画在组合输入后;提示符宽度另加而不参与待编辑字符串。 let caret_x = prefix + text::measure(&format!("{column}{}", state.preedit), TextStyle::code()).width; + // 当前输入行加上输出行数得到整个 transcript 中的纵向位置,滚动量稍后统一扣除。 let caret_y = (first_input as f32 + row) * ROW; // 新输出触发 Follow,但显式 reveal_input 优先,避免执行后或编辑时光标被自动滚动隐藏。 let follow = state.follow && output_changed && !state.reveal_input; // 把现有滚动量钳入“光标可见”的允许范围,而非每次将光标强制居中;横向预留 24 单位空间。 if state.reveal_input { + // 编辑触发的 reveal 优先保住光标,不跳到历史尾部;校正后清标记,只消费一次请求。 let s = &mut state.scroll[Region::Transcript as usize]; s.y = s.y.clamp( (caret_y + ROW + 16.0 - area.height).max(0.0).min(caret_y), @@ -154,8 +178,10 @@ pub(super) fn build( s.x = s.x.clamp((caret_x + 24.0 - area.width).max(0.0), caret_x); state.reveal_input = false; } + // Painter 测量完整内容并钳制最终滚动,必须在此之后再读取 scroll 计算输入命中布局。 p.lines(area, Region::Transcript, &lines, state, follow); let scroll = state.scroll[Region::Transcript as usize]; + // 保存输入正文第一行的实际屏幕原点,包含内容边距、提示符、输出高度及最终两轴滚动。 let origin = Point::new( area.x + 8.0 + prefix - scroll.x, area.y + 5.0 + first_input as f32 * ROW - scroll.y, @@ -165,10 +191,12 @@ pub(super) fn build( viewport: area, origin, }); + // 输入可见竖向片段与 transcript 相交,下面只为交集注册 Focus(Input),不覆盖历史文本区域。 let input_top = origin.y.max(area.y); let input_bottom = (origin.y + state.input.split('\n').count() as f32 * ROW).min(area.y + area.height - 9.0); // 仅为仍可见的输入片段注册文本输入命中区,扣除滚动条空间,避免历史输出区误抢输入焦点。 + // 整个输入已滚出视口时不登记输入矩形,避免不可见编辑区域拦截鼠标。 if input_bottom > input_top { p.ui.inline_input( Rect::new( @@ -181,6 +209,7 @@ pub(super) fn build( ); } // 光标单独绘制并限制在文本视口内,不能穿过滚动条或工具页边缘;随后配对 PopClip。 + // 光标只是绘制覆盖层,先压入扣除滚动条的 clip,再画一像素竖线并弹出,不扩大输入命中范围。 if state.focus == Some(Focus::Input) { p.ui.list.push(DrawCommand::PushClip(Rect::new( area.x, diff --git a/src/shell/developer_tools/network.rs b/src/shell/developer_tools/network.rs index 89177c8..9668f67 100644 --- a/src/shell/developer_tools/network.rs +++ b/src/shell/developer_tools/network.rs @@ -4,23 +4,27 @@ use super::*; /// 将本进程 HTTP 活动投影为可筛选请求列表和独立滚动详情,并生成选择/分隔条命中区域。 +// developer_tools::build 选择 Network 时调用,共享日志已在父入口克隆,过滤和文字测量不持活动日志锁。 pub(super) fn build( p: &mut Painter, state: &mut DeveloperToolsState, records: &[Exchange], viewport: Rect, ) { + // 工具页以 viewport 横向原点和宽度布局;底部状态栏高度留给父入口统一绘制。 let x = viewport.x; let w = viewport.width; let bottom = viewport.height - 22.0; p.fill(Rect::new(x, 32.0, w, 30.0), BAR); + // 水位后存在记录才允许 Clear,Action::ClearNetwork 更新工具水位并清除选择,不删除 Console 共用日志。 p.enabled_button( Rect::new(x + 3.0, 32.0, 62.0, 29.0), "Clear", Action::ClearNetwork, records.iter().any(|r| r.id > state.network_after), ); + // 过滤框注册 Filter 焦点,按键编辑在 DeveloperToolsState 中处理;此处只展示当前文本与焦点。 let filter_area = Rect::new(x + 72.0, 35.0, (w - 85.0).max(100.0), 24.0); p.ui.text_field( filter_area, @@ -33,6 +37,7 @@ pub(super) fn build( &format!( "Filter: {}{}", state.filter, + // 竖线是焦点提示字符,不是 Console 那种按字节光标定位,也不修改过滤字符串本身。 if state.focus == Some(Focus::Filter) { "│" } else { @@ -54,10 +59,12 @@ pub(super) fn build( let divider = 92.0 + (bottom - 92.0) * state.network_fraction; // 对方法、路径和状态的组合字符串做大小写不敏感子串匹配;未响应记录可用 pending 搜索。 let filter = state.filter.to_lowercase(); + // 保留原记录顺序及引用,只建立本次展示候选,不复制/更改每条 Exchange 的响应状态。 let visible = records .iter() // 清屏按 ID 水位隐藏旧记录,不删除共享 ActivityLog,Console 仍可独立展示同一条请求。 .filter(|r| r.id > state.network_after) + // 方法、路径与状态合并后统一小写匹配,origin 和正文不在当前过滤范围内。 .filter(|r| { format!( "{} {} {}", @@ -70,7 +77,9 @@ pub(super) fn build( }) .collect::>(); let mut lines = Vec::new(); + // 逐匹配记录生成一行,pending 状态用省略号;耗时直接取日志快照,不在绘制时重新计时。 for r in &visible { + // 先创建带状态颜色的行,再附加稳定 Request 动作和选中标记,筛选改变行号也不会改变目标身份。 let mut l = Line::new( format!( "{:<7} {:<6} {:>5} ms {}", @@ -97,6 +106,7 @@ pub(super) fn build( MUTED, )); } + // 列表使用 Requests Region 保存独立滚动,false 表示不因为新增请求自动跟随到底部。 p.lines( Rect::new(x + 1.0, 92.0, w - 2.0, divider - 92.0), Region::Requests, @@ -104,12 +114,15 @@ pub(super) fn build( state, false, ); + // 只登记横向分隔条,父绘制入口最后覆盖绘制并注册 Split::Network 命中目标。 p.split(Rect::new(x + 1.0, divider, w - 2.0, 1.0), Split::Network); // 详情从完整保留记录中查找,因此筛选隐藏某行后可继续查看已选详情;记录淘汰后才回提示。 let selected = records .iter() .find(|r| Some(r.id) == state.selected_request); + // 详情另建行列表,不依赖上面的 visible;筛选无匹配并不自动取消已选请求。 let mut details = Vec::new(); + // 仍保留的已选记录交给 request_details;ID 已淘汰或从未选择则显示捕获范围说明。 if let Some(r) = selected { request_details(&mut details, r); } else { diff --git a/src/shell/developer_tools/settings.rs b/src/shell/developer_tools/settings.rs index 74dd411..8a85d3e 100644 --- a/src/shell/developer_tools/settings.rs +++ b/src/shell/developer_tools/settings.rs @@ -4,7 +4,9 @@ use super::*; /// 绘制强调色候选、自定义颜色编辑框及主题预览;实际应用和错误校验由输入动作处理。 +// developer_tools::build 选择 Settings 时调用,产生工具控件和 Action;颜色解析与主题应用由输入处理路径完成。 pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: Rect) { + // 强调色、编辑框和预览使用同一工具坐标原点,宽度来自宿主分配,不读取业务 Page 布局。 let x = viewport.x; let w = viewport.width; @@ -18,6 +20,7 @@ pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: ); // 色块动作携带调色板索引,绘制仅比较当前 accent 标记选中;实际修改由统一动作处理器执行。 for (i, color) in PALETTE.iter().enumerate() { + // 每个调色板项固定宽度和间距,索引 i 作为 Action::Theme 的参数与 PALETTE 一一对应。 let r = Rect::new(x + 24.0 + i as f32 * 44.0, 122.0, 32.0, 32.0); p.ui.swatch(r, Action::Theme(i), *color, *color == state.accent); } @@ -41,6 +44,7 @@ pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: &format!( "{}{}", state.custom, + // 尾部竖线只提示焦点,state.custom 仍是未提交编辑值,不能因显示该值就认为主题已更新。 if state.focus == Some(Focus::Custom) { "│" } else { @@ -52,6 +56,7 @@ pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: ); // 校验错误由输入处理路径写入,设置页只显示;错误不会在每次重绘时重复执行或清除。 + // 只有已有错误才画错误行,不在 build 中重新校验或清除,保留用户提交操作的反馈。 if !state.input_error.is_empty() { p.label(x + 24.0, 245.0, &state.input_error, ERROR, false); }