docs: clarify developer workspace editing and request presentation
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user