docs: explain developer workspace state and input lifecycle
This commit is contained in:
@@ -33,6 +33,7 @@ mod sources;
|
||||
use painter::*;
|
||||
use selection::SourceSelection;
|
||||
|
||||
// Console、Painter 和 SourceSelection 共用逻辑行高;显示行号、光标导航和滚动必须采用同一值。
|
||||
const ROW: f32 = 19.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
@@ -46,6 +47,7 @@ pub enum Tab {
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 管理面板中可区分的输入/列表/详情区域。
|
||||
// 枚举值同时作为 scroll/widths 的数组下标,新增区域时须同步调整两组数组长度。
|
||||
pub enum Region {
|
||||
Transcript = 0,
|
||||
Tree,
|
||||
@@ -54,7 +56,7 @@ pub enum Region {
|
||||
Details,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
/// 工具区域保留的滚动偏移与内容范围。
|
||||
/// 工具区域保留的两轴滚动偏移;内容范围由本帧布局另行保存。
|
||||
struct Scroll {
|
||||
x: f32,
|
||||
y: f32,
|
||||
@@ -93,6 +95,7 @@ pub enum Split {
|
||||
/// 正在捕获的工具拖动操作,移动时保持原目标。
|
||||
enum Drag {
|
||||
Split(Split),
|
||||
// 记录区域、是否横轴以及手柄位移转内容位移的倍率;倍率来自 Painter,而不是每次移动重新估算。
|
||||
Scroll(Region, bool, f32),
|
||||
}
|
||||
|
||||
@@ -100,12 +103,15 @@ enum Drag {
|
||||
/// Only appearance and the window-level launcher are shared.
|
||||
/// 多工具页的状态拥有者,切换时保存/恢复当前页数据。
|
||||
pub struct Workspace {
|
||||
// 数组顺序与 new/select 的工具映射一致,每个槽独立保留输入、日志水位、选区和滚动。
|
||||
states: [DeveloperToolsState; 4],
|
||||
active: usize,
|
||||
}
|
||||
impl Workspace {
|
||||
/// 为 Console/Sources/Network/Settings 各创建一份独立状态,共享事件循环代理,初始选择 Console。
|
||||
// shell/app 创建窗口宿主状态时调用,代理克隆让各工具的异步请求错误能唤醒同一个原生事件循环。
|
||||
pub fn new(proxy: winit::event_loop::EventLoopProxy<AppEvent>) -> Self {
|
||||
// 对四种工具各创建一个默认状态,再覆盖 tab/proxy,避免多个工具误共享一个可编辑 input。
|
||||
let states = [Tab::Console, Tab::Sources, Tab::Network, Tab::Settings].map(|tab| {
|
||||
DeveloperToolsState {
|
||||
tab,
|
||||
@@ -116,7 +122,9 @@ impl Workspace {
|
||||
Self { states, active: 0 }
|
||||
}
|
||||
/// 选择一个工具页并恢复它各自保留的输入、焦点和滚动状态。
|
||||
// scene::build_scene 根据活动页面的 developer_tool 标记调用;None 关闭工具显示,未知非空名字使用 Console 槽。
|
||||
pub fn select(&mut self, name: Option<&str>) {
|
||||
// 名字仅选择已有状态槽,不在这里创建 VM 页面;页面复用/创建通过 initialize_tools 的能力调用完成。
|
||||
let next = match name {
|
||||
Some("Sources") => 1,
|
||||
Some("Network") => 2,
|
||||
@@ -136,12 +144,14 @@ impl Workspace {
|
||||
impl std::ops::Deref for Workspace {
|
||||
type Target = DeveloperToolsState;
|
||||
/// 将包装类型的只读访问转交给内部当前上下文。
|
||||
// scene/app 的字段读取和 build 通过当前槽访问状态,不会在一次读取时遍历其他工具。
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.states[self.active]
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for Workspace {
|
||||
/// 将包装类型的可变访问转交给内部当前上下文。
|
||||
// 原生输入事件调用当前槽方法,切换工具前 blur 旧槽解除交互状态。
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.states[self.active]
|
||||
}
|
||||
@@ -150,11 +160,16 @@ impl std::ops::DerefMut for Workspace {
|
||||
#[derive(Debug)]
|
||||
/// 一份工具页面的输入、选择、滚动、焦点和显示配置。
|
||||
pub struct DeveloperToolsState {
|
||||
// 是否当前展示管理页;关闭时 build 返回空列表/布局,状态本身仍保留供后续重新打开。
|
||||
pub open: bool,
|
||||
// 最近工具逻辑坐标,cursor_kind 复用它判断文本区或控件悬停。
|
||||
pointer: Point,
|
||||
source_selection: SourceSelection,
|
||||
// 底层宿主控件焦点、悬停及按下捕获;与文本编辑 focus/drag 配合,不直接改变业务 VM。
|
||||
pub interaction: Interaction,
|
||||
// 绘制 reconcile 或源码拖选置位,shell/app 据此安排额外重绘,修正失效控件外观。
|
||||
pub repaint: bool,
|
||||
// 后台线程只携带事件代理和自有文本,不借用窗口或 DeveloperToolsState。
|
||||
pub proxy: Option<winit::event_loop::EventLoopProxy<AppEvent>>,
|
||||
pub cloud_sources: SourceWorkspace,
|
||||
pub tab: Tab,
|
||||
@@ -165,25 +180,31 @@ pub struct DeveloperToolsState {
|
||||
pub cursor: usize,
|
||||
// 输入框表示全选状态,Sources 则结合 SourceSelection 表示全选;并非通用起止字节范围。
|
||||
pub selection: bool,
|
||||
// IME 未提交的组合文本,只在 Console 显示副本插入;Commit 后才通过 insert 写正式 input。
|
||||
pub preedit: String,
|
||||
pub filter: String,
|
||||
pub custom: String,
|
||||
pub accent: Color,
|
||||
pub input_error: String,
|
||||
// Sources 左栏宽度及 Network 上下比例分别控制分隔布局,拖动只调整工具空间。
|
||||
tree_width: f32,
|
||||
network_fraction: f32,
|
||||
scroll: [Scroll; 5],
|
||||
// 各 Region 独立保存行文本指纹与最大宽度,颜色/选择改变无需重测,文本改变才失效。
|
||||
widths: [Option<(u64, f32)>; 5],
|
||||
// 下一帧需要显示编辑光标,console::build 测量位置后消费;不是立即修改滚动的命令。
|
||||
reveal_input: bool,
|
||||
// Console 输出行指纹,用于新增输出时 Follow;当前未提交 input 不参与这份指纹。
|
||||
console_stamp: u64,
|
||||
drag: Option<Drag>,
|
||||
cloud: bool,
|
||||
follow: bool,
|
||||
// 稳定日志 ID,筛选隐藏行仍可保留详情;日志淘汰后详情页回到提示。
|
||||
selected_request: Option<u64>,
|
||||
// 清屏水位只隐藏旧记录,与共享 ActivityLog 的实际淘汰分开;Network 有独立水位。
|
||||
console_after: u64,
|
||||
network_after: u64,
|
||||
// 最多 80 条成功启动发送线程的输入,不等于成功执行历史;失败线程可能再由事件恢复输入。
|
||||
history: Vec<String>,
|
||||
history_index: Option<usize>,
|
||||
// 首次进入历史导航前保存当前未提交输入,向下越过最新历史后恢复此草稿。
|
||||
@@ -191,6 +212,7 @@ pub struct DeveloperToolsState {
|
||||
}
|
||||
impl Default for DeveloperToolsState {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
// Workspace::new 用它构造四份独立状态;初始不开工具、无焦点,默认启用 Follow 并请求显示输入。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
@@ -232,20 +254,26 @@ impl Default for DeveloperToolsState {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 本帧工具面板与子区域的几何,用于绘制和原生事件命中。
|
||||
pub struct DeveloperToolsLayout {
|
||||
// 整工具画布的逻辑区域;shell/app 先据此决定输入是否属于管理控件。
|
||||
pub panel: Rect,
|
||||
pub widgets: Vec<Target<Action>>,
|
||||
splits: Vec<(Rect, Split)>,
|
||||
// 各行面板的区域和完整内容宽高,wheel 查命中,Painter 重绘时根据内容限制最大偏移。
|
||||
regions: Vec<(Rect, Region, f32, f32)>,
|
||||
// 滚动手柄矩形及内容/手柄位移倍率,press 找到后保存到 Drag::Scroll。
|
||||
thumbs: Vec<(Rect, Region, bool, f32)>,
|
||||
input: Option<ConsoleInputLayout>,
|
||||
// 源码可见区、已扣滚动/行号的正文原点和完整显示文本,鼠标选择与复制共享本帧数据。
|
||||
source: Option<(Rect, Point, Vec<String>)>,
|
||||
}
|
||||
impl DeveloperToolsLayout {
|
||||
/// 将工具局部绘制/交互布局平移到实际窗口位置。
|
||||
// scene::build_scene 把工具移动到侧栏右边时调用;显示列表另以设备比例平移,这里保持逻辑坐标。
|
||||
pub fn translate(&mut self, dx: f32, dy: f32) {
|
||||
// 绘制列表已由外层按设备像素平移,这里所有命中矩形和文字原点都按逻辑单位同步移动。
|
||||
self.panel.x += dx;
|
||||
self.panel.y += dy;
|
||||
// 先同步所有通用 Widget 命中,后面再移动额外的分隔条、区域和手柄矩形。
|
||||
for target in &mut self.widgets {
|
||||
target.rect.x += dx;
|
||||
target.rect.y += dy;
|
||||
@@ -262,12 +290,14 @@ impl DeveloperToolsLayout {
|
||||
rect.x += dx;
|
||||
rect.y += dy;
|
||||
}
|
||||
// 输入视口与正文原点必须一起移动,才能让鼠标插入点和 IME 候选窗对齐显示内容。
|
||||
if let Some(input) = &mut self.input {
|
||||
input.viewport.x += dx;
|
||||
input.viewport.y += dy;
|
||||
input.origin.x += dx;
|
||||
input.origin.y += dy;
|
||||
}
|
||||
// 源码正文行文本不变,只移动选择的可见区域和文字原点,字节端点不随窗口平移变化。
|
||||
if let Some((area, origin, _)) = &mut self.source {
|
||||
area.x += dx;
|
||||
area.y += dy;
|
||||
@@ -283,7 +313,8 @@ struct ConsoleInputLayout {
|
||||
origin: Point,
|
||||
}
|
||||
impl DeveloperToolsState {
|
||||
/// 用最新完整源文档更新工具投影,维护 Sources 与 Console 历史的一致性。
|
||||
/// 根据最新源文档修正 Sources 的已选 AST 节点,保留仍有效的选择。
|
||||
// scene 构建工具画面前调用,仅修正 AST 选择身份;输入与 HTTP 历史来自另外的状态和活动日志。
|
||||
pub fn document_updated(&mut self, doc: &RenderDocument) {
|
||||
// 已选 AST ID 在新文档不存在时回退根;有效选择继续保留,不按行号盲目关联另一份代码。
|
||||
if self
|
||||
@@ -295,8 +326,10 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
/// 根据当前工具命中区域选择文本、拖动或默认系统光标。
|
||||
// shell/app 更新原生光标时调用;Sources 文本区域优先返回 I-beam,其余用同帧 Widget 状态映射。
|
||||
pub fn cursor_kind(&self, layout: &DeveloperToolsLayout) -> winit::window::CursorIcon {
|
||||
use winit::window::CursorIcon;
|
||||
// 工具关闭时不让旧布局或悬停状态影响原生光标。
|
||||
if !self.open {
|
||||
return CursorIcon::Default;
|
||||
}
|
||||
@@ -307,6 +340,7 @@ impl DeveloperToolsState {
|
||||
{
|
||||
return CursorIcon::Text;
|
||||
}
|
||||
// 宿主抽象 Cursor 转换成 winit CursorIcon,保留禁用、抓取与横纵调整等语义。
|
||||
match self.interaction.cursor(&layout.widgets) {
|
||||
Cursor::Default => CursorIcon::Default,
|
||||
Cursor::Pointer => CursorIcon::Pointer,
|
||||
@@ -317,11 +351,13 @@ impl DeveloperToolsState {
|
||||
Cursor::Disabled => CursorIcon::NotAllowed,
|
||||
}
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
/// 委托宿主控件处理指针离开,具体捕获是否保留由 Interaction 决定。
|
||||
// shell/app 收到原生指针离开窗口时调用,委托 Interaction 清理离开状态,不清空输入文本。
|
||||
pub fn pointer_left(&mut self) {
|
||||
self.interaction.pointer_left();
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
// 工具切换和窗口焦点转移路径调用,清焦点/拖动/preedit,保留输入草稿、历史及滚动。
|
||||
pub fn blur(&mut self) {
|
||||
self.interaction.blur();
|
||||
self.drag = None;
|
||||
@@ -329,7 +365,9 @@ impl DeveloperToolsState {
|
||||
self.preedit.clear();
|
||||
}
|
||||
/// 记录按下时的控件或文本选择目标,为释放激活和拖动保留捕获。
|
||||
// shell/app 路由到工具的鼠标按下入口;仅当前打开面板内输入能建立控件捕获或源码拖选。
|
||||
pub fn press(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
// 不处理工具外按下,避免旧命中几何抢走业务页面交互。
|
||||
if !self.open || !layout.panel.contains(p) {
|
||||
return;
|
||||
}
|
||||
@@ -337,6 +375,7 @@ impl DeveloperToolsState {
|
||||
self.preedit.clear();
|
||||
self.pointer = p;
|
||||
self.interaction.pointer_moved(p, &layout.widgets);
|
||||
// 先交给同帧 Widget 集合解析,滚动条和分隔条等控件命中优先于大面积源码文字区域。
|
||||
let target = self.interaction.press(&layout.widgets);
|
||||
self.focus = None;
|
||||
if let Some((area, origin, lines)) = &layout.source
|
||||
@@ -348,8 +387,10 @@ impl DeveloperToolsState {
|
||||
self.selection = false;
|
||||
self.source_selection.begin(p, *origin, lines);
|
||||
}
|
||||
// 不同角色决定按下即开始拖动、立即给文本焦点,或仅保留捕获等待释放激活。
|
||||
if let Some(target) = target {
|
||||
match target.role {
|
||||
// 细分隔线的交互矩形向四周扩 3 单位,布局条目提供拖动类别而不是用屏幕位置猜测。
|
||||
Role::Splitter(_) => {
|
||||
if let Some((_, split)) = layout.splits.iter().find(|(r, _)| {
|
||||
Rect::new(r.x - 3.0, r.y - 3.0, r.width + 6.0, r.height + 6.0).contains(p)
|
||||
@@ -357,6 +398,7 @@ impl DeveloperToolsState {
|
||||
self.drag = Some(Drag::Split(*split));
|
||||
}
|
||||
}
|
||||
// 找到对应手柄时复制区域、轴与倍率,之后指针移出手柄仍对同一区域拖动。
|
||||
Role::Scrollbar(_) => {
|
||||
if let Some((_, region, h, ratio)) =
|
||||
layout.thumbs.iter().find(|(r, _, _, _)| r.contains(p))
|
||||
@@ -379,6 +421,7 @@ impl DeveloperToolsState {
|
||||
activity: &Activity,
|
||||
) {
|
||||
self.focus = None;
|
||||
// 所有确认的控件动作集中处理;绘制模块只构造 Action,不在创建控件时执行请求或主题更新。
|
||||
match action {
|
||||
Some(Action::Focus(f)) => {
|
||||
self.focus = Some(f);
|
||||
@@ -389,12 +432,14 @@ impl DeveloperToolsState {
|
||||
// 根据本帧输入原点和固定行高定位行,逐字素测量字节结束位置决定插入点,不能按等宽字符数量切中文。
|
||||
let row = (((p.y - input.origin.y) / ROW).floor().max(0.0) as usize)
|
||||
.min(self.input.split('\n').count() - 1);
|
||||
// 累计前面各行的 UTF-8 字节长度及换行字节,行内测得 offset 后再合成整个 input 的 cursor。
|
||||
let mut start = 0;
|
||||
let line = self
|
||||
.input
|
||||
.split('\n')
|
||||
.enumerate()
|
||||
.find_map(|(i, line)| {
|
||||
// 命中目标行就借用该行,未到达时累加它连同分隔换行占用的字节。
|
||||
if i == row {
|
||||
Some(line)
|
||||
} else {
|
||||
@@ -403,11 +448,14 @@ impl DeveloperToolsState {
|
||||
}
|
||||
})
|
||||
.unwrap_or("");
|
||||
// 转换到正文的横向距离,点击提示符或左边距按非负目标处理;行内定位使用字素宽度。
|
||||
let target = (p.x - input.origin.x).max(0.0);
|
||||
let mut offset = line.len();
|
||||
// 与 Sources 的中点算法不同,这里取首个末端宽度不小于目标的字素起点。
|
||||
for (i, part) in line.grapheme_indices(true) {
|
||||
let advance =
|
||||
text::measure(&line[..i + part.len()], TextStyle::code()).width;
|
||||
// 找到包含目标 x 的字素即停止;超过全行时 offset 维持行尾。
|
||||
if advance >= target {
|
||||
offset = i;
|
||||
break;
|
||||
@@ -423,10 +471,12 @@ impl DeveloperToolsState {
|
||||
self.scroll[Region::Transcript as usize] = Scroll::default();
|
||||
self.reveal_cursor();
|
||||
}
|
||||
// 清屏同时取消详情选择,但不改变共享日志或 Console 的清屏水位。
|
||||
Some(Action::ClearNetwork) => {
|
||||
self.network_after = activity.lock().unwrap().records.back().map_or(0, |r| r.id);
|
||||
self.selected_request = None;
|
||||
}
|
||||
// 切换后清输出指纹,使下一次 Console 构建重新判断跟随;滚动操作本身在绘制时完成。
|
||||
Some(Action::Follow) => {
|
||||
self.follow = !self.follow;
|
||||
self.console_stamp = 0;
|
||||
@@ -435,10 +485,12 @@ impl DeveloperToolsState {
|
||||
Some(Action::Snapshot) => self.cloud = false,
|
||||
Some(Action::Cloud) => self.cloud = true,
|
||||
Some(Action::CloudRefresh) => self.cloud_sources.refresh(),
|
||||
// provider 同步读取结果保存在 cloud_sources,无论成功与否都把源码滚动移回起点。
|
||||
Some(Action::CloudOpen(id)) => {
|
||||
self.cloud_sources.open(&id);
|
||||
self.scroll[Region::Source as usize] = Scroll::default();
|
||||
}
|
||||
// 选中 AST 身份并跳到一基起始行,saturating_sub 兼容零值而不发生无符号下溢。
|
||||
Some(Action::Node(id, line)) => {
|
||||
self.selected_node_id = Some(id);
|
||||
self.scroll[Region::Source as usize].x = 0.0;
|
||||
@@ -454,6 +506,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
self.scroll[Region::Details as usize] = Scroll::default();
|
||||
}
|
||||
// 索引来自 PALETTE 色块生成的动作,直接更新强调色并把自定义编辑文本同步成六位 hex。
|
||||
Some(Action::Theme(i)) => {
|
||||
self.accent = PALETTE[i];
|
||||
self.custom = format!(
|
||||
@@ -465,17 +518,20 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
/// 结束捕获并按同一目标/启用条件决定是否执行操作。
|
||||
// shell/app 的鼠标抬起入口;先由 Interaction 验证捕获目标,再统一结束工具拖动与源码拖选。
|
||||
pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
self.pointer = p;
|
||||
self.interaction.pointer_moved(p, &layout.widgets);
|
||||
let action = self.interaction.release(&layout.widgets);
|
||||
self.drag = None;
|
||||
self.source_selection.dragging = false;
|
||||
// 只有返回有效动作才 activate,否则只释放捕获,不因抬起点恰好碰到别的控件触发它。
|
||||
if action.is_some() {
|
||||
self.activate(action, layout, p, activity);
|
||||
}
|
||||
}
|
||||
/// 按工具控件焦点处理 Tab、确认等管理键盘交互。
|
||||
// shell/app 在普通编辑 key 前调用;返回 true 表示已经消费控件导航/激活,不再落入 Console 文本逻辑。
|
||||
pub fn keyboard_control(
|
||||
&mut self,
|
||||
event: &winit::event::KeyEvent,
|
||||
@@ -491,6 +547,7 @@ impl DeveloperToolsState {
|
||||
if !self.open || !self.preedit.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// 没有文本焦点时 Escape 清宿主控件焦点;按下和抬起都消费,只有按下执行 blur。
|
||||
if event.logical_key == Key::Named(NamedKey::Escape) && self.focus.is_none() {
|
||||
if event.state == ElementState::Pressed {
|
||||
self.interaction.blur();
|
||||
@@ -502,6 +559,7 @@ impl DeveloperToolsState {
|
||||
&& (self.focus != Some(Focus::Input) || shift)
|
||||
{
|
||||
if event.state == ElementState::Pressed && !event.repeat {
|
||||
// 仅非重复按下推进焦点;目标含 Focus 动作才映射成文本焦点,否则保留控件导航焦点。
|
||||
let target = self.interaction.focus_next(&layout.widgets, shift);
|
||||
self.focus = target.and_then(|t| match t.action {
|
||||
Some(Action::Focus(f)) => Some(f),
|
||||
@@ -514,6 +572,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Enter/Space 仅激活非 TextField 控件,文本输入的这些键继续交 key 处理。
|
||||
let control = self
|
||||
.interaction
|
||||
.focused(&layout.widgets)
|
||||
@@ -530,6 +589,7 @@ impl DeveloperToolsState {
|
||||
self.interaction.key_press(&layout.widgets);
|
||||
}
|
||||
} else {
|
||||
// 抬起时结束键盘捕获并取动作,按下自动重复不会反复执行按钮。
|
||||
let action = self.interaction.key_release(&layout.widgets);
|
||||
self.activate(action, layout, Point::default(), activity);
|
||||
}
|
||||
@@ -538,6 +598,7 @@ impl DeveloperToolsState {
|
||||
false
|
||||
}
|
||||
/// 更新工具内悬停、拖动、选择或分隔条位置。
|
||||
// shell/app 每次指针移动传入当前位置和增量,更新悬停、源码拖选及已捕获拖动;不重新选择 Drag 目标。
|
||||
pub fn move_pointer(
|
||||
&mut self,
|
||||
p: Point,
|
||||
@@ -559,11 +620,14 @@ impl DeveloperToolsState {
|
||||
self.source_selection.extend(p, *origin, lines);
|
||||
self.repaint = true;
|
||||
}
|
||||
// 分隔条按绝对指针位置计算,滚动条按增量乘捕获倍率计算,两种几何语义分开。
|
||||
match self.drag {
|
||||
// 树宽至少 120,且尽量给右侧源码保留 180;窄窗口通过 max 保持 clamp 上下界合法。
|
||||
Some(Drag::Split(Split::Tree)) => {
|
||||
self.tree_width =
|
||||
(p.x - layout.panel.x).clamp(120.0, (layout.panel.width - 180.0).max(120.0))
|
||||
}
|
||||
// 把指针 y 转成上下区比例并限于 20%..80%,避免拖动把某个区域压到完全不可访问。
|
||||
Some(Drag::Split(Split::Network)) => {
|
||||
self.network_fraction = ((p.y - layout.panel.y - 92.0)
|
||||
/ (layout.panel.height - 114.0).max(1.0))
|
||||
@@ -585,6 +649,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
/// 将滚轮作用到当前工具的可滚动区域,钳制本工具偏移。
|
||||
// shell/app 路由工具区域滚轮时调用;按逆区域顺序命中一个面板,内容上界交下一次 Painter 统一计算。
|
||||
pub fn wheel(&mut self, layout: &DeveloperToolsLayout, p: Point, dx: f32, dy: f32) {
|
||||
// 滚轮寻找最前景包含点的 Region,只钳下限;上限在下一轮绘制根据最新内容范围统一钳制。
|
||||
if let Some((_, r, _, _)) = layout
|
||||
@@ -602,6 +667,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
/// 在当前选择/光标处插入输入文本,并更新光标与可见范围。
|
||||
// 普通文本输入、IME Commit、粘贴及剪切成功后的清空共用;Source 焦点与无焦点不接受写入。
|
||||
pub fn insert(&mut self, value: &str) {
|
||||
match self.focus {
|
||||
Some(Focus::Input) => {
|
||||
@@ -610,6 +676,7 @@ impl DeveloperToolsState {
|
||||
self.input_error = "Input exceeds 1 MiB".into();
|
||||
return;
|
||||
}
|
||||
// Input 全选替换只在预算检查通过后清原文,因此超大粘贴失败仍保留已有输入。
|
||||
if self.selection {
|
||||
self.input.clear();
|
||||
self.cursor = 0;
|
||||
@@ -621,6 +688,7 @@ impl DeveloperToolsState {
|
||||
self.history_index = None;
|
||||
self.reveal_cursor();
|
||||
}
|
||||
// 过滤框只追加无控制字符,并把请求列表纵向滚动归零;不改变已选请求身份。
|
||||
Some(Focus::Filter) => {
|
||||
if self.selection {
|
||||
self.filter.clear();
|
||||
@@ -631,6 +699,7 @@ impl DeveloperToolsState {
|
||||
.extend(value.chars().filter(|c| !c.is_control()).take(200));
|
||||
self.scroll[Region::Requests as usize].y = 0.0;
|
||||
}
|
||||
// 自定义色是编辑文本,允许的字符过滤不等于完整 hex 验证,Enter 才应用主题。
|
||||
Some(Focus::Custom) => {
|
||||
if self.selection {
|
||||
self.custom.clear();
|
||||
@@ -653,8 +722,10 @@ impl DeveloperToolsState {
|
||||
self.reveal_input = true;
|
||||
}
|
||||
/// 返回输入法候选窗应跟随的逻辑光标位置。
|
||||
// shell/app 更新原生输入法候选位置时调用,使用本帧 Console 输入原点和正式 input 的 cursor,不追加 preedit。
|
||||
pub fn ime_position(&self, layout: &DeveloperToolsLayout) -> Point {
|
||||
if let Some(input) = layout.input {
|
||||
// cursor 由编辑路径保持在有效字节边界,当前行前缀测量用于候选窗跟随,不按字符数估算 x。
|
||||
let before = &self.input[..self.cursor];
|
||||
let row = before.matches('\n').count() as f32;
|
||||
let col = before.rsplit('\n').next().unwrap_or("");
|
||||
@@ -665,9 +736,11 @@ impl DeveloperToolsState {
|
||||
(input.origin.y + row * ROW).clamp(area.y, area.y + area.height),
|
||||
);
|
||||
}
|
||||
// 没有输入布局时返回工具内固定候选位置,不通过空 input 几何构造无效坐标。
|
||||
Point::new(layout.panel.x + 20.0, 60.0)
|
||||
}
|
||||
/// 取得当前工具文本选择内容,供复制命令使用。
|
||||
// shell/app 的复制/剪切快捷键调用,返回 String 后才交系统剪贴板;Source 只读插入不会修改显示源码。
|
||||
pub fn selected_text(&self) -> String {
|
||||
// 源码支持任意拖选区间或全选;其他三个文本框当前只支持整框全选复制。
|
||||
if self.focus == Some(Focus::Source) {
|
||||
@@ -684,14 +757,17 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
/// 处理编辑键和导航键,维护 UTF-8 光标/选择边界。
|
||||
// shell/app 处理命名编辑键调用;输入法组合期间外层已过滤原始编辑键,提交文本走 insert。
|
||||
pub fn key(&mut self, key: &winit::keyboard::NamedKey, shift: bool, command: bool) {
|
||||
use winit::keyboard::NamedKey::*;
|
||||
// Escape 在任何文本焦点先处理,结束输入组合与宿主控件焦点,不提交 Console 草稿。
|
||||
if *key == Escape {
|
||||
self.focus = None;
|
||||
self.preedit.clear();
|
||||
self.interaction.blur();
|
||||
return;
|
||||
}
|
||||
// 源码使用独立 SourceSelection 导航,不把只读源码端点写成 Console cursor;成功移动安排重绘。
|
||||
if self.focus == Some(Focus::Source) {
|
||||
if let Some(row) = self.source_selection.navigate(key, shift, command) {
|
||||
self.selection = false;
|
||||
@@ -702,6 +778,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
// 应用主题时解析六位 RGB 并调整过亮强调色,失败保留原主题并写输入错误。
|
||||
if self.focus == Some(Focus::Custom) && *key == Enter {
|
||||
// 允许开头一个或多个 # 被剥除,余下必须恰好六个 ASCII hex 字节;不是单纯按输入字符过滤就接受。
|
||||
let hex = self.custom.trim_start_matches('#');
|
||||
if hex.len() == 6
|
||||
&& let Ok(v) = u32::from_str_radix(hex, 16)
|
||||
@@ -713,6 +790,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 其他文本框仅处理 Backspace,删除末尾 Unicode 标量或全选内容,没有独立光标导航。
|
||||
if self.focus != Some(Focus::Input) {
|
||||
if *key == Backspace {
|
||||
match self.focus {
|
||||
@@ -736,11 +814,13 @@ impl DeveloperToolsState {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Console 的 Enter/Tab/Space、删除和导航共用字节 cursor;末尾统一请求下一帧显示光标。
|
||||
match key {
|
||||
Enter if !shift => self.submit(),
|
||||
Enter => self.insert("\n"),
|
||||
Tab => self.insert(" "),
|
||||
Space => self.insert(" "),
|
||||
// 全选先删整个输入,否则分别找前/后完整字素边界,只替换对应字节区间。
|
||||
Backspace | Delete => {
|
||||
if self.selection {
|
||||
self.input.clear();
|
||||
@@ -763,6 +843,7 @@ impl DeveloperToolsState {
|
||||
self.input.replace_range(self.cursor..self.cursor + len, "");
|
||||
}
|
||||
}
|
||||
// command 直接到输入开头,普通按键退一个字素;这套 Console 选择当前只支持全选,移动会取消它。
|
||||
ArrowLeft => {
|
||||
self.cursor = if command {
|
||||
0
|
||||
@@ -774,6 +855,7 @@ impl DeveloperToolsState {
|
||||
};
|
||||
self.selection = false;
|
||||
}
|
||||
// command 直接到输入末尾,否则加后缀第一个完整字素的字节长度,行分隔也按文本字素遍历。
|
||||
ArrowRight => {
|
||||
self.cursor = if command {
|
||||
self.input.len()
|
||||
@@ -786,6 +868,7 @@ impl DeveloperToolsState {
|
||||
};
|
||||
self.selection = false;
|
||||
}
|
||||
// 普通 Home 使用当前行前一个换行之后的位置,command 则到整份输入的字节 0。
|
||||
Home => {
|
||||
self.cursor = if command {
|
||||
0
|
||||
@@ -794,6 +877,7 @@ impl DeveloperToolsState {
|
||||
};
|
||||
self.selection = false;
|
||||
}
|
||||
// 普通 End 找当前 cursor 后首个换行,command 则用完整字符串长度。
|
||||
End => {
|
||||
self.cursor = if command {
|
||||
self.input.len()
|
||||
@@ -813,12 +897,14 @@ impl DeveloperToolsState {
|
||||
if self.history_index.is_none() {
|
||||
self.draft = self.input.clone();
|
||||
}
|
||||
// 未进入历史时视为位于列表末端,向上进最近一项;向下越过末端恢复事先保存草稿。
|
||||
let current = self.history_index.unwrap_or(self.history.len());
|
||||
let index = if *key == ArrowUp {
|
||||
current.saturating_sub(1)
|
||||
} else {
|
||||
(current + 1).min(self.history.len())
|
||||
};
|
||||
// 仅真实历史下标存 Some,末端用 None 表示草稿;恢复文本后光标统一放到末尾。
|
||||
self.history_index = (index < self.history.len()).then_some(index);
|
||||
self.input = self
|
||||
.history_index
|
||||
@@ -829,6 +915,7 @@ impl DeveloperToolsState {
|
||||
let start = self.input[..self.cursor].rfind('\n').map_or(0, |i| i + 1);
|
||||
// 多行垂直导航保持字素列数,不保持像素 x;目标行较短时自然停到行尾。
|
||||
let col = self.input[start..self.cursor].graphemes(true).count();
|
||||
// 求相邻行的起始字节位置;第一行向上取 0,末行向下取文本末尾。
|
||||
let target = if *key == ArrowUp {
|
||||
if start == 0 {
|
||||
0
|
||||
@@ -840,6 +927,7 @@ impl DeveloperToolsState {
|
||||
.find('\n')
|
||||
.map_or(self.input.len(), |i| self.cursor + i + 1)
|
||||
};
|
||||
// 限制目标行切片到下一个换行,不让列数继续消耗到后续行,较短行自然停在行尾。
|
||||
let end = self.input[target..]
|
||||
.find('\n')
|
||||
.map_or(self.input.len(), |i| target + i);
|
||||
@@ -860,12 +948,15 @@ impl DeveloperToolsState {
|
||||
self.reveal_cursor();
|
||||
}
|
||||
/// 在线程中经本地 REPL 请求打开指定内置工具,页面复用/创建由 VM 的 developerTools 实现负责。
|
||||
// shell/app 首次打开或请求工具入口时调用,后台向当前 app.main REPL 发送能力调用,不在 GUI 线程操作 VM。
|
||||
pub fn initialize_tools(&mut self, name: &str) {
|
||||
// 把名称与代理克隆进后台线程,线程不借用 Workspace 或活动 Window 生命周期。
|
||||
let name = name.to_owned();
|
||||
let proxy = self.proxy.clone();
|
||||
thread::spawn(move || {
|
||||
// 用调试字符串转义名称生成 Endor 文本,再经本地 REPL 执行;实际工具白名单由宿主能力验证。
|
||||
let source = format!("ui.developerTools({name:?})");
|
||||
// 连接本机固定 API,读取响应直到服务端关闭;这里只检查 I/O 成功,不解析 HTTP 状态或结果 JSON。
|
||||
let send = || -> std::io::Result<()> {
|
||||
let mut stream = TcpStream::connect_timeout(
|
||||
&"127.0.0.1:4737".parse().unwrap(),
|
||||
@@ -882,6 +973,7 @@ impl DeveloperToolsState {
|
||||
stream.read_to_string(&mut response)?;
|
||||
Ok(())
|
||||
};
|
||||
// 传输失败且有事件代理时通知 ConsoleFailed;初始化调用没有输入草稿,source 传空串。
|
||||
if let Err(error) = send()
|
||||
&& let Some(proxy) = proxy
|
||||
{
|
||||
@@ -893,14 +985,18 @@ impl DeveloperToolsState {
|
||||
});
|
||||
}
|
||||
/// 把当前工具输入按所选模式提交给本地 API,并异步反馈结果。
|
||||
// Console key 的不带 Shift Enter 使用此入口,模式固定为普通持续会话提交。
|
||||
fn submit(&mut self) {
|
||||
self.submit_console(false);
|
||||
}
|
||||
/// 提交 Console 输入到持久运行时,而不是把全部历史重新执行一遍。
|
||||
// 普通 Enter 和 ResetSession 共用,reset 选择 DELETE,普通提交用 POST,二者只发送当前输入一次。
|
||||
fn submit_console(&mut self, reset: bool) {
|
||||
// 空白输入不提交,reset 即使没有输入也有效;返回前不改已有输入/历史状态。
|
||||
if !reset && self.input.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
// 普通提交克隆草稿,线程后续失败可带原文返回;重置不发送用户草稿内容。
|
||||
let source = if reset {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -909,6 +1005,7 @@ impl DeveloperToolsState {
|
||||
let proxy = self.proxy.clone();
|
||||
// 后台仅发送一次当前输入或 DELETE 重置;源通过正文传递,传输失败带回原 source 让 GUI 恢复输入。
|
||||
// 线程成功启动不等于源执行成功,HTTP 结果由 ActivityLog 在 Console 展示。
|
||||
// 下方紧凑闭包只负责 HTTP 传输:2 秒连接、120 秒读取和 10 秒写入超时,响应正文不在客户端解析。
|
||||
let spawned=thread::Builder::new().name("tessera-console-client".into()).spawn(move||{
|
||||
let send=||->std::io::Result<()> {
|
||||
let mut stream=TcpStream::connect_timeout(&"127.0.0.1:4737".parse().unwrap(),Duration::from_secs(2))?;
|
||||
@@ -919,11 +1016,13 @@ impl DeveloperToolsState {
|
||||
};
|
||||
if let Err(error)=send()&&let Some(proxy)=proxy{let _=proxy.send_event(AppEvent::ConsoleFailed{message:error.to_string(),source});}
|
||||
});
|
||||
// 按线程创建结果更新 GUI 状态,不等待网络或 VM 执行;失败创建线程时保留输入并显示错误。
|
||||
match spawned {
|
||||
// 发送线程启动成功才清输入并加入最多80条历史;之后若网络失败由 ConsoleFailed 反馈,不能把清框当执行成功。
|
||||
Ok(_) => {
|
||||
if !reset {
|
||||
self.history.push(self.input.clone());
|
||||
// 只留最近 80 次已启动提交,超额从最旧项移除;这与共享 HTTP 活动日志的淘汰分别记账。
|
||||
if self.history.len() > 80 {
|
||||
self.history.remove(0);
|
||||
}
|
||||
@@ -942,6 +1041,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
|
||||
/// 按当前工具生成设备比例显示列表与逻辑坐标命中布局,绘制后校正跨帧焦点和捕获。
|
||||
// scene::build_scene 传入设备像素宽高和比例,返回设备显示列表加逻辑命中布局,外层再加侧栏横向偏移。
|
||||
pub fn build(
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -950,6 +1050,7 @@ pub fn build(
|
||||
state: &mut DeveloperToolsState,
|
||||
activity: &Activity,
|
||||
) -> (DisplayList, DeveloperToolsLayout) {
|
||||
// 无工具页面时不读取活动日志或生成控件,空 layout 同时移除当前可命中区域。
|
||||
if !state.open {
|
||||
return (Default::default(), Default::default());
|
||||
}
|
||||
@@ -959,6 +1060,7 @@ pub fn build(
|
||||
let w = width;
|
||||
let x = 0.0;
|
||||
let panel = Rect::new(x, 0.0, w, height);
|
||||
// Painter 克隆当前 Interaction 供控件外观和目标生成;最后 reconcile 在原 state 上清理失效焦点。
|
||||
let mut p = Painter {
|
||||
ui: Ui::new(Theme::new(state.accent), state.interaction.clone()),
|
||||
layout: DeveloperToolsLayout {
|
||||
@@ -991,6 +1093,7 @@ pub fn build(
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let bottom = height - 22.0;
|
||||
// 仅当前选中的子工具构建内容,共用这次 records 快照和 Painter,底栏与分隔条仍由本入口收尾。
|
||||
match state.tab {
|
||||
Tab::Console => console::build(&mut p, state, &records, panel),
|
||||
Tab::Sources => sources::build(&mut p, state, doc, panel),
|
||||
@@ -1023,6 +1126,7 @@ pub fn build(
|
||||
// Splitters must be above toolbars, pane backgrounds and status bars for their
|
||||
// whole length. Register them last so their hit priority matches their paint order.
|
||||
// 分隔条最后绘制/注册,命中优先级与可见叠放相同,细线扩展命中区不会被后画的背景遮住。
|
||||
// 逆登记顺序绘制覆盖层,拖动 Widget ID 包含 Split 类别,与布局条目保持同一身份。
|
||||
for (rect, kind) in p.layout.splits.clone().into_iter().rev() {
|
||||
p.ui.splitter(
|
||||
rect,
|
||||
@@ -1036,7 +1140,9 @@ pub fn build(
|
||||
p.ui.list.push(DrawCommand::PopClip);
|
||||
// 把本管理 UI 当前使用的矩形、文字、裁剪、描边逐一转设备比例,几何与文字样式同倍率缩放。
|
||||
let mut scaled = DisplayList::default();
|
||||
// 只转换当前宿主管理 UI 实际生成的变体,其他命令原样克隆;新增变体若含坐标需检查这里的 scale 规则。
|
||||
for cmd in p.ui.list.commands() {
|
||||
// 矩形转换闭包统一缩放原点和尺寸,文字还需单独缩放样式,描边还需缩放半径/线宽。
|
||||
let rect =
|
||||
|r: &Rect| Rect::new(r.x * scale, r.y * scale, r.width * scale, r.height * scale);
|
||||
scaled.push(match cmd {
|
||||
@@ -1075,6 +1181,7 @@ pub fn build(
|
||||
other => other.clone(),
|
||||
});
|
||||
}
|
||||
// 移交同帧逻辑目标集合,不能从缩放后的显示列表重新猜控件几何。
|
||||
p.layout.widgets = p.ui.targets;
|
||||
// 根据新目标集合清理失效焦点/捕获,若视觉状态变化要求再绘一帧,避免控件消失后保持旧按压外观。
|
||||
state.repaint = state.interaction.reconcile(&p.layout.widgets);
|
||||
|
||||
Reference in New Issue
Block a user