docs: clarify developer workspace editing and request presentation

This commit is contained in:
2026-11-12 20:19:57 +08:00
parent edf513bff7
commit 1f26349436
2 changed files with 33 additions and 0 deletions
+28
View File
@@ -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);