docs: explain Tessera modules and execution flows in Chinese
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。
|
||||
//! 正文预览和允许显示的头部受限制,避免活动面板成为无限日志或暴露不必要数据的通道。
|
||||
//!
|
||||
//! Bounded observation of this process's local HTTP API, never external traffic.
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -10,6 +13,7 @@ pub const BODY_PREVIEW: usize = 32 * 1024;
|
||||
const MAX_RECORDS: usize = 80;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。
|
||||
pub struct Exchange {
|
||||
pub id: u64,
|
||||
pub method: String,
|
||||
@@ -26,11 +30,13 @@ pub struct Exchange {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。
|
||||
pub struct ActivityLog {
|
||||
pub records: VecDeque<Exchange>,
|
||||
next_id: u64,
|
||||
}
|
||||
impl ActivityLog {
|
||||
/// 为请求分配活动 ID 并登记其方法、来源和受限头部。
|
||||
pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 {
|
||||
self.next_id += 1;
|
||||
let id = self.next_id;
|
||||
@@ -63,11 +69,13 @@ impl ActivityLog {
|
||||
}
|
||||
id
|
||||
}
|
||||
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
|
||||
pub fn body(&mut self, id: u64, body: &str) {
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.request = preview(body);
|
||||
}
|
||||
}
|
||||
/// 完成本模块当前阶段的结果整理,把后续工作交回调用方。
|
||||
pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) {
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.status = Some(status);
|
||||
@@ -78,6 +86,7 @@ impl ActivityLog {
|
||||
}
|
||||
}
|
||||
|
||||
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
|
||||
pub fn preview(value: &str) -> String {
|
||||
if value.len() <= BODY_PREVIEW {
|
||||
return value.to_owned();
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! winit 原生应用生命周期和窗口事件入口。这里创建窗口、发布逻辑视口、转发输入,并把不可变文档画到原生表面。
|
||||
//! 窗口线程不执行 Endor 业务回调;共享状态通过锁读取,实际 VM 更新由 runtime worker 串行完成。
|
||||
//! 侧栏、页面与开发工具有各自命中区域。拖动捕获必须保留最初的视口目标,不能随指针经过其他组件而跳转。
|
||||
//!
|
||||
use std::{
|
||||
num::NonZeroU32,
|
||||
sync::{Arc, RwLock},
|
||||
@@ -24,6 +28,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// worker 到原生事件循环的通知,携带更新/错误/退出状态而不是可执行 VM 引用。
|
||||
pub enum AppEvent {
|
||||
RuntimeFailed { message: String },
|
||||
DocumentUpdated,
|
||||
@@ -31,6 +36,7 @@ pub enum AppEvent {
|
||||
ConsoleFailed { message: String, source: String },
|
||||
}
|
||||
|
||||
/// 一个原生窗口的宿主状态;主窗口还协调子窗口资源,共用同一运行时与输入通道。
|
||||
pub struct TesseraApp {
|
||||
resource: u64,
|
||||
control: crate::api::RuntimeControl,
|
||||
@@ -63,6 +69,7 @@ pub struct TesseraApp {
|
||||
}
|
||||
|
||||
impl TesseraApp {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
activity: Activity,
|
||||
@@ -79,6 +86,7 @@ impl TesseraApp {
|
||||
))
|
||||
}
|
||||
|
||||
/// 建立窗口宿主状态并复用权威输入客户端,避免子窗口使用滞后的独立事件队列。
|
||||
fn with_event_client(
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
activity: Activity,
|
||||
@@ -118,6 +126,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将已发布窗口文档与原生资源对齐;session 更换时丢弃旧域的子窗口。
|
||||
fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.resource != crate::window::MAIN_WINDOW {
|
||||
return;
|
||||
@@ -183,6 +192,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 只应用当前运行时身份的窗口命令;执行前从共享队列取走,避免重复执行。
|
||||
fn apply_window_commands(&mut self) {
|
||||
let Ok(mut namespace) = self.namespace.write() else {
|
||||
return;
|
||||
@@ -236,6 +246,7 @@ impl TesseraApp {
|
||||
self.publish_viewport();
|
||||
}
|
||||
|
||||
/// 将真实原生窗口尺寸换算成扣除侧栏的逻辑页面尺寸,并发布给 worker。
|
||||
fn publish_viewport(&self) {
|
||||
let Some(window) = &self.window else {
|
||||
return;
|
||||
@@ -280,12 +291,14 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅请求原生事件循环安排重绘,不在调用处立即递归绘制。
|
||||
fn request_redraw(&self) {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过托管开发工具入口打开指定工具页面,保留对应窗口上下文。
|
||||
fn open_developer_tool(&mut self, name: &str) {
|
||||
let input = self.namespace.read().ok().map(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
@@ -313,6 +326,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 接收完整文档更新,协调子窗口和原生命令,再更新输入焦点并请求重绘。
|
||||
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.reconcile_windows(event_loop);
|
||||
self.apply_window_commands();
|
||||
@@ -343,6 +357,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 先处理宿主管理输入、选择与快捷键,再按焦点决定是否交给页面。
|
||||
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
|
||||
let command = self.modifiers.super_key() || self.modifiers.control_key();
|
||||
if command
|
||||
@@ -486,6 +501,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 判断指针是否被侧栏或管理面板覆盖,阻止一次点击同时落入页面。
|
||||
fn guest_covered(&self, point: Point) -> bool {
|
||||
if self.sidebar.covers(point) {
|
||||
return true;
|
||||
@@ -501,6 +517,7 @@ impl TesseraApp {
|
||||
.contains(point)
|
||||
}
|
||||
|
||||
/// 排除宿主管理区域后,把原生指针转换到页面使用的坐标系。
|
||||
fn guest_point(&self) -> Option<Point> {
|
||||
if self.guest_covered(self.pointer) {
|
||||
return None;
|
||||
@@ -518,6 +535,7 @@ impl TesseraApp {
|
||||
Some(current.document.frame.surface.to_world(point))
|
||||
}
|
||||
|
||||
/// 从页面坐标与当前帧命中结果构造带版本的点击输入。
|
||||
fn guest_hit(&self) -> Option<crate::events::Input> {
|
||||
let point = self.guest_point()?;
|
||||
let current = self.namespace.read().ok()?;
|
||||
@@ -540,6 +558,7 @@ impl TesseraApp {
|
||||
})
|
||||
}
|
||||
|
||||
/// 将标签快捷键转换为当前窗口/帧中的明确页面动作。
|
||||
fn send_tab_shortcut(&mut self, close: bool) -> bool {
|
||||
let input = self.namespace.read().ok().and_then(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
@@ -577,6 +596,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理侧栏展开、工具入口和页面管理,尺寸变化还需重新发布可用视口。
|
||||
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
|
||||
use crate::sidebar::Action;
|
||||
match action {
|
||||
@@ -619,6 +639,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 在手势开始时捕获具体相机视口 ID,后续拖动保持同一目标。
|
||||
fn begin_pan(&mut self) -> bool {
|
||||
self.pan_viewport = None;
|
||||
let Some(point) = self.guest_point() else {
|
||||
@@ -632,6 +653,7 @@ impl TesseraApp {
|
||||
self.pan_viewport.is_some() || frame.surface.mode != crate::page::Mode::Window
|
||||
}
|
||||
|
||||
/// 把窗口位移/缩放转换为捕获视口的纯数据相机事件,处理侧栏和页面视角的尺度差异。
|
||||
fn page_view(&mut self, kind: &str, delta: [f32; 2]) {
|
||||
if !self.panning && self.guest_covered(self.pointer) {
|
||||
return;
|
||||
@@ -724,6 +746,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把已经构造的页面输入交给有界客户端,保持 GUI 线程不直接调用 VM。
|
||||
fn send_guest(&mut self, event: crate::events::Input) {
|
||||
if !self.event_client.send(event) {
|
||||
self.developer_tools.input_error = "Native event queue is unavailable or full".into();
|
||||
@@ -731,6 +754,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把页面拥有焦点时的键盘动作转换为托管事件,保留按下/释放配对。
|
||||
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
|
||||
let Key::Named(key) = &event.logical_key else {
|
||||
return false;
|
||||
@@ -800,6 +824,7 @@ impl TesseraApp {
|
||||
false
|
||||
}
|
||||
|
||||
/// 按平台窗口属性创建原生窗口、软件表面和输入法支持,再公布实际逻辑视口。
|
||||
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
|
||||
if self.window.is_some() {
|
||||
return Ok(());
|
||||
@@ -847,6 +872,7 @@ impl TesseraApp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止新的交互并请求运行时清理;原生事件循环等待完成通知后退出。
|
||||
fn request_exit(&mut self) {
|
||||
if self.exit_requested {
|
||||
return;
|
||||
@@ -863,6 +889,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把原生应用记录的最终错误反馈给 main 的退出结果。
|
||||
pub fn exit_result(&self) -> anyhow::Result<()> {
|
||||
if let Some(error) = &self.native_error {
|
||||
anyhow::bail!("{error}");
|
||||
@@ -870,6 +897,7 @@ impl TesseraApp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 包装一次实际绘制,统一记录故障并避免对同一错误反复发送通知。
|
||||
fn redraw(&mut self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
@@ -891,6 +919,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取完整发布文档,生成整窗显示列表,写入原生像素表面并呈现;错误交由外层记录。
|
||||
fn try_redraw(&mut self) -> anyhow::Result<()> {
|
||||
let guest_cursor = self.guest_hit().is_some()
|
||||
|| self
|
||||
@@ -956,6 +985,7 @@ impl TesseraApp {
|
||||
}
|
||||
|
||||
impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
/// 处理 OS 直接结束事件循环的收尾,尝试在明确时限内等待运行时清理。
|
||||
fn exiting(&mut self, _: &ActiveEventLoop) {
|
||||
// OS termination can bypass CloseRequested. The loop is already ending;
|
||||
// use a bounded fallback rather than silently dropping every callback.
|
||||
@@ -973,8 +1003,10 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前不在进入等待前轮询工作;文档通知和原生事件负责唤醒循环,避免空闲忙转。
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
|
||||
|
||||
/// 应用获得原生活跃上下文时建立窗口,并仅在需要时请求初始页面。
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.resource == crate::window::MAIN_WINDOW
|
||||
&& !self.startup_page_requested
|
||||
@@ -998,6 +1030,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理 worker 发回的文档更新、故障和退出完成通知,不在 GUI 线程执行源回调。
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
|
||||
if self.control.is_exiting() && !matches!(&event, AppEvent::ShutdownComplete { .. }) {
|
||||
return;
|
||||
@@ -1029,6 +1062,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理原生尺寸、输入、焦点和关闭事件;先判断宿主控件,再转发页面事件。
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_event_loop: &ActiveEventLoop,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! 把 UTF-8 文本读写适配到当前平台的系统剪贴板命令。
|
||||
//! 外部命令失败作为普通错误交给调用方处理;这里不解析或执行剪贴板中的程序文本。
|
||||
//!
|
||||
//! Clipboard text is always UTF-8, including Finder/LaunchServices launches without LANG.
|
||||
use anyhow::{Result, bail};
|
||||
#[cfg(target_os = "macos")]
|
||||
/// 运行选定平台的剪贴板命令并检查其退出状态。
|
||||
fn command(program: &str) -> std::process::Command {
|
||||
let mut cmd = std::process::Command::new(program);
|
||||
cmd.env("LC_ALL", "en_US.UTF-8")
|
||||
@@ -8,6 +12,7 @@ fn command(program: &str) -> std::process::Command {
|
||||
.env("LANG", "en_US.UTF-8");
|
||||
cmd
|
||||
}
|
||||
/// 取得系统剪贴板的 UTF-8 文本,失败作为可显示错误返回。
|
||||
pub fn read() -> Result<String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -26,6 +31,7 @@ pub fn read() -> Result<String> {
|
||||
bail!("system clipboard adapter is not available on this platform")
|
||||
}
|
||||
}
|
||||
/// 将提供的文本交给系统剪贴板命令,不执行文本内容。
|
||||
pub fn write(value: &str) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Console/Sources/Network/Settings 的宿主交互协调与绘制状态。
|
||||
//! 工具标签有独立焦点、滚动和文本选择状态;源程序与结果来自当前发布文档,提交动作通过本地 API 进入 worker。
|
||||
//! 输入法预编辑、光标、选择和控件捕获分别处理,避免把界面操作直接当成业务模型写入。
|
||||
//!
|
||||
//! Native developer workspace. Coordinates and scroll offsets are logical pixels.
|
||||
use crate::{
|
||||
activity::{Activity, Exchange},
|
||||
@@ -32,6 +36,7 @@ use selection::SourceSelection;
|
||||
const ROW: f32 = 19.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
/// 当前管理工具种类,决定使用 Console/Sources/Network/Settings 哪份状态。
|
||||
pub enum Tab {
|
||||
#[default]
|
||||
Console,
|
||||
@@ -40,6 +45,7 @@ pub enum Tab {
|
||||
Settings,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 管理面板中可区分的输入/列表/详情区域。
|
||||
pub enum Region {
|
||||
Transcript = 0,
|
||||
Tree,
|
||||
@@ -48,11 +54,13 @@ pub enum Region {
|
||||
Details,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
/// 工具区域保留的滚动偏移与内容范围。
|
||||
struct Scroll {
|
||||
x: f32,
|
||||
y: f32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 当前工具内接受键盘输入的目标类别。
|
||||
pub enum Focus {
|
||||
Source,
|
||||
Input,
|
||||
@@ -60,6 +68,7 @@ pub enum Focus {
|
||||
Custom,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
/// 工具控件激活后执行的管理动作,避免在绘制时产生业务副作用。
|
||||
pub enum Action {
|
||||
ClearConsole,
|
||||
ClearNetwork,
|
||||
@@ -75,11 +84,13 @@ pub enum Action {
|
||||
Theme(usize),
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 工具画面分隔布局的可调整方向/区域。
|
||||
pub enum Split {
|
||||
Tree,
|
||||
Network,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 正在捕获的工具拖动操作,移动时保持原目标。
|
||||
enum Drag {
|
||||
Split(Split),
|
||||
Scroll(Region, bool, f32),
|
||||
@@ -87,11 +98,13 @@ enum Drag {
|
||||
|
||||
/// Each workspace keeps its own input, focus, selection, scroll and request state.
|
||||
/// Only appearance and the window-level launcher are shared.
|
||||
/// 多工具页的状态拥有者,切换时保存/恢复当前页数据。
|
||||
pub struct Workspace {
|
||||
states: [DeveloperToolsState; 4],
|
||||
active: usize,
|
||||
}
|
||||
impl Workspace {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(proxy: winit::event_loop::EventLoopProxy<AppEvent>) -> Self {
|
||||
let states = [Tab::Console, Tab::Sources, Tab::Network, Tab::Settings].map(|tab| {
|
||||
DeveloperToolsState {
|
||||
@@ -102,6 +115,7 @@ impl Workspace {
|
||||
});
|
||||
Self { states, active: 0 }
|
||||
}
|
||||
/// 选择一个工具页并恢复它各自保留的输入、焦点和滚动状态。
|
||||
pub fn select(&mut self, name: Option<&str>) {
|
||||
let next = match name {
|
||||
Some("Sources") => 1,
|
||||
@@ -120,17 +134,20 @@ impl Workspace {
|
||||
}
|
||||
impl std::ops::Deref for Workspace {
|
||||
type Target = DeveloperToolsState;
|
||||
/// 将包装类型的只读访问转交给内部当前上下文。
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.states[self.active]
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for Workspace {
|
||||
/// 将包装类型的可变访问转交给内部当前上下文。
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.states[self.active]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 一份工具页面的输入、选择、滚动、焦点和显示配置。
|
||||
pub struct DeveloperToolsState {
|
||||
pub open: bool,
|
||||
pointer: Point,
|
||||
@@ -167,6 +184,7 @@ pub struct DeveloperToolsState {
|
||||
draft: String,
|
||||
}
|
||||
impl Default for DeveloperToolsState {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
@@ -206,6 +224,7 @@ impl Default for DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 本帧工具面板与子区域的几何,用于绘制和原生事件命中。
|
||||
pub struct DeveloperToolsLayout {
|
||||
pub panel: Rect,
|
||||
pub widgets: Vec<Target<Action>>,
|
||||
@@ -216,6 +235,7 @@ pub struct DeveloperToolsLayout {
|
||||
source: Option<(Rect, Point, Vec<String>)>,
|
||||
}
|
||||
impl DeveloperToolsLayout {
|
||||
/// 将工具局部绘制/交互布局平移到实际窗口位置。
|
||||
pub fn translate(&mut self, dx: f32, dy: f32) {
|
||||
self.panel.x += dx;
|
||||
self.panel.y += dy;
|
||||
@@ -250,11 +270,13 @@ impl DeveloperToolsLayout {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Console 文字输入、光标与可见范围的本帧排版。
|
||||
struct ConsoleInputLayout {
|
||||
viewport: Rect,
|
||||
origin: Point,
|
||||
}
|
||||
impl DeveloperToolsState {
|
||||
/// 用最新完整源文档更新工具投影,维护 Sources 与 Console 历史的一致性。
|
||||
pub fn document_updated(&mut self, doc: &RenderDocument) {
|
||||
if self
|
||||
.selected_node_id
|
||||
@@ -264,6 +286,7 @@ impl DeveloperToolsState {
|
||||
self.selected_node_id = doc.root.as_ref().map(|n| n.id.clone());
|
||||
}
|
||||
}
|
||||
/// 根据当前工具命中区域选择文本、拖动或默认系统光标。
|
||||
pub fn cursor_kind(&self, layout: &DeveloperToolsLayout) -> winit::window::CursorIcon {
|
||||
use winit::window::CursorIcon;
|
||||
if !self.open {
|
||||
@@ -286,15 +309,18 @@ impl DeveloperToolsState {
|
||||
Cursor::Disabled => CursorIcon::NotAllowed,
|
||||
}
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
pub fn pointer_left(&mut self) {
|
||||
self.interaction.pointer_left();
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
pub fn blur(&mut self) {
|
||||
self.interaction.blur();
|
||||
self.drag = None;
|
||||
self.focus = None;
|
||||
self.preedit.clear();
|
||||
}
|
||||
/// 记录按下时的控件或文本选择目标,为释放激活和拖动保留捕获。
|
||||
pub fn press(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
if !self.open || !layout.panel.contains(p) {
|
||||
return;
|
||||
@@ -333,6 +359,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 执行已确认激活的管理控件动作,区分工具切换、源码操作和设置。
|
||||
fn activate(
|
||||
&mut self,
|
||||
action: Option<Action>,
|
||||
@@ -423,6 +450,7 @@ impl DeveloperToolsState {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
/// 结束捕获并按同一目标/启用条件决定是否执行操作。
|
||||
pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
self.pointer = p;
|
||||
self.interaction.pointer_moved(p, &layout.widgets);
|
||||
@@ -433,6 +461,7 @@ impl DeveloperToolsState {
|
||||
self.activate(action, layout, p, activity);
|
||||
}
|
||||
}
|
||||
/// 按工具控件焦点处理 Tab、确认等管理键盘交互。
|
||||
pub fn keyboard_control(
|
||||
&mut self,
|
||||
event: &winit::event::KeyEvent,
|
||||
@@ -491,6 +520,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
false
|
||||
}
|
||||
/// 更新工具内悬停、拖动、选择或分隔条位置。
|
||||
pub fn move_pointer(
|
||||
&mut self,
|
||||
p: Point,
|
||||
@@ -535,6 +565,7 @@ impl DeveloperToolsState {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
/// 将滚轮作用到当前工具的可滚动区域,钳制本工具偏移。
|
||||
pub fn wheel(&mut self, layout: &DeveloperToolsLayout, p: Point, dx: f32, dy: f32) {
|
||||
if let Some((_, r, _, _)) = layout
|
||||
.regions
|
||||
@@ -550,6 +581,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 在当前选择/光标处插入输入文本,并更新光标与可见范围。
|
||||
pub fn insert(&mut self, value: &str) {
|
||||
match self.focus {
|
||||
Some(Focus::Input) => {
|
||||
@@ -591,9 +623,11 @@ impl DeveloperToolsState {
|
||||
Some(Focus::Source) | None => {}
|
||||
}
|
||||
}
|
||||
/// 调整输入区滚动位置,让当前光标保持在可见范围内。
|
||||
pub(crate) fn reveal_cursor(&mut self) {
|
||||
self.reveal_input = true;
|
||||
}
|
||||
/// 返回输入法候选窗应跟随的逻辑光标位置。
|
||||
pub fn ime_position(&self, layout: &DeveloperToolsLayout) -> Point {
|
||||
if let Some(input) = layout.input {
|
||||
let before = &self.input[..self.cursor];
|
||||
@@ -608,6 +642,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
Point::new(layout.panel.x + 20.0, 60.0)
|
||||
}
|
||||
/// 取得当前工具文本选择内容,供复制命令使用。
|
||||
pub fn selected_text(&self) -> String {
|
||||
if self.focus == Some(Focus::Source) {
|
||||
return self.source_selection.text(self.selection);
|
||||
@@ -622,6 +657,7 @@ impl DeveloperToolsState {
|
||||
Some(Focus::Source) | None => String::new(),
|
||||
}
|
||||
}
|
||||
/// 处理编辑键和导航键,维护 UTF-8 光标/选择边界。
|
||||
pub fn key(&mut self, key: &winit::keyboard::NamedKey, shift: bool, command: bool) {
|
||||
use winit::keyboard::NamedKey::*;
|
||||
if *key == Escape {
|
||||
@@ -792,6 +828,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
self.reveal_cursor();
|
||||
}
|
||||
/// 初始化内置工具对应的页面和展示状态,避免重复建立同一组工具。
|
||||
pub fn initialize_tools(&mut self, name: &str) {
|
||||
let name = name.to_owned();
|
||||
let proxy = self.proxy.clone();
|
||||
@@ -823,9 +860,11 @@ impl DeveloperToolsState {
|
||||
}
|
||||
});
|
||||
}
|
||||
/// 把当前工具输入按所选模式提交给本地 API,并异步反馈结果。
|
||||
fn submit(&mut self) {
|
||||
self.submit_console(false);
|
||||
}
|
||||
/// 提交 Console 输入到持久运行时,而不是把全部历史重新执行一遍。
|
||||
fn submit_console(&mut self, reset: bool) {
|
||||
if !reset && self.input.trim().is_empty() {
|
||||
return;
|
||||
@@ -867,6 +906,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制 Console 输入、结果历史和对应命中区域。
|
||||
//! 这里消费工具状态并生成显示列表;用户程序的编译执行由提交路径和 worker 完成。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制已观察到的 HTTP 活动、请求列表与详情。
|
||||
//! 筛选、选择和滚动是工具视图状态;展示一条请求不会重发该请求。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
//! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。
|
||||
//! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 开发工具的绘制助手,把布局结果写入同帧显示列表。
|
||||
pub(super) struct Painter {
|
||||
pub(super) ui: Ui<Action>,
|
||||
pub(super) layout: DeveloperToolsLayout,
|
||||
}
|
||||
impl Painter {
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn fill(&mut self, r: Rect, c: Color) {
|
||||
self.ui.fill(r, c);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn label(&mut self, x: f32, y: f32, value: &str, c: Color, code: bool) {
|
||||
self.ui.label(x, y, value, c, code);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn button(&mut self, r: Rect, label: &str, action: Action, active: bool) {
|
||||
let role = match action {
|
||||
Action::Follow | Action::Snapshot | Action::Cloud => Role::Toggle,
|
||||
@@ -19,14 +26,17 @@ impl Painter {
|
||||
self.ui
|
||||
.button(r, action, Button::new(label).selected(active).role(role));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn enabled_button(&mut self, r: Rect, label: &str, action: Action, enabled: bool) {
|
||||
self.ui
|
||||
.button(r, action, Button::new(label).enabled(enabled));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn split(&mut self, r: Rect, kind: Split) {
|
||||
// Paint and register splitters after all pane content, in one overlay pass.
|
||||
self.layout.splits.push((r, kind));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn lines(
|
||||
&mut self,
|
||||
area: Rect,
|
||||
@@ -132,6 +142,7 @@ impl Painter {
|
||||
self.scrollbar(area, region, false, s.y, content_h);
|
||||
self.scrollbar(area, region, true, s.x, content_w);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn scrollbar(
|
||||
&mut self,
|
||||
r: Rect,
|
||||
@@ -170,6 +181,7 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
/// 一行可绘制、可关联源码位置的工具文本。
|
||||
pub(super) struct Line {
|
||||
pub(super) value: String,
|
||||
pub(super) color: Color,
|
||||
@@ -178,11 +190,13 @@ pub(super) struct Line {
|
||||
pub(super) action: Option<Action>,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
/// 管理画面内分隔条方向/位置描述。
|
||||
pub(super) enum Divider {
|
||||
Accent,
|
||||
Neutral,
|
||||
}
|
||||
impl Line {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub(super) fn new(value: impl Into<String>, color: Color) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
@@ -193,6 +207,7 @@ impl Line {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 按行绘制工具文本,返回后续布局需要的占用范围。
|
||||
pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
|
||||
out.extend(
|
||||
value
|
||||
@@ -201,6 +216,7 @@ pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
|
||||
.map(|s| Line::new(s, color)),
|
||||
);
|
||||
}
|
||||
/// 把一条 Console 结果投影为可滚动、可选择的文字画面。
|
||||
pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(body) {
|
||||
if value["execution"] == "repl" {
|
||||
@@ -263,6 +279,7 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
|
||||
}
|
||||
multiline(out, body, color);
|
||||
}
|
||||
/// 将代码层次展开为工具可绘制的行数据,保留节点与源范围关联。
|
||||
pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize, &'a CodeNode)>) {
|
||||
out.push((depth, node));
|
||||
for child in &node.children {
|
||||
@@ -270,6 +287,7 @@ pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize
|
||||
}
|
||||
}
|
||||
|
||||
/// 绘制选中请求的头部、正文预览与响应信息,不重新执行该请求。
|
||||
pub(super) fn request_details(out: &mut Vec<Line>, r: &Exchange) {
|
||||
multiline(
|
||||
out,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 多行文本选择和光标导航。选择端点映射到合法字符边界,避免把 UTF-8 字节中间当成一个字符。
|
||||
//! 工具只保留当前文本行和选择范围;复制操作取得的是所选内容,不是整份运行时状态。
|
||||
//!
|
||||
//! Read-only, grapheme-aware source selection, independent of console editing.
|
||||
use crate::{
|
||||
geometry::Point,
|
||||
@@ -6,6 +9,7 @@ use crate::{
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 行文本、锚点与选择端点,既供鼠标拖选也供键盘导航使用。
|
||||
pub(super) struct SourceSelection {
|
||||
lines: Vec<String>,
|
||||
anchor: (usize, usize),
|
||||
@@ -13,6 +17,7 @@ pub(super) struct SourceSelection {
|
||||
pub dragging: bool,
|
||||
}
|
||||
impl SourceSelection {
|
||||
/// 替换可选择文本行,并把旧选择限制在新文本仍有效的位置。
|
||||
pub fn set_lines(&mut self, lines: &[String]) {
|
||||
if self.lines != lines {
|
||||
self.lines = lines.to_vec();
|
||||
@@ -21,6 +26,7 @@ impl SourceSelection {
|
||||
self.dragging = false;
|
||||
}
|
||||
}
|
||||
/// 把工具指针位置换算成合法行号与文本字符边界。
|
||||
fn locate(p: Point, origin: Point, lines: &[String]) -> (usize, usize) {
|
||||
let row = (((p.y - origin.y) / super::ROW).floor().max(0.0) as usize)
|
||||
.min(lines.len().saturating_sub(1));
|
||||
@@ -39,14 +45,17 @@ impl SourceSelection {
|
||||
}
|
||||
(row, byte)
|
||||
}
|
||||
/// 在指针位置建立文本选择锚点,开始一次拖动选择。
|
||||
pub fn begin(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
self.anchor = Self::locate(p, origin, lines);
|
||||
self.end = self.anchor;
|
||||
self.dragging = true;
|
||||
}
|
||||
/// 保持原锚点,随指针更新选择另一端。
|
||||
pub fn extend(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
self.end = Self::locate(p, origin, lines);
|
||||
}
|
||||
/// 用键盘移动文本选择端点,按扩展模式决定是否保留锚点。
|
||||
pub fn navigate(
|
||||
&mut self,
|
||||
key: &winit::keyboard::NamedKey,
|
||||
@@ -109,6 +118,7 @@ impl SourceSelection {
|
||||
}
|
||||
Some(row)
|
||||
}
|
||||
/// 返回排序后的有效选择范围,隐藏拖动方向差异。
|
||||
fn range(&self, all: bool) -> ((usize, usize), (usize, usize)) {
|
||||
if all {
|
||||
return (
|
||||
@@ -121,6 +131,7 @@ impl SourceSelection {
|
||||
}
|
||||
(self.anchor.min(self.end), self.anchor.max(self.end))
|
||||
}
|
||||
/// 拼接当前选择范围内的实际文本,保持多行换行与 UTF-8 边界。
|
||||
pub fn text(&self, all: bool) -> String {
|
||||
let (a, b) = self.range(all);
|
||||
self.lines
|
||||
@@ -135,6 +146,7 @@ impl SourceSelection {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
/// 取得某行的可见选择片段,供高亮绘制复用。
|
||||
pub fn span(&self, row: usize, all: bool, lines: &[String]) -> Option<(f32, f32)> {
|
||||
let (a, b) = self.range(all);
|
||||
if row < a.0 || row > b.0 || a == b {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制宿主设置页面和主题选择区域。
|
||||
//! 这里只投影当前设置和交互目标,平台原生窗口行为由对应系统适配层负责。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: Rect) {
|
||||
let x = viewport.x;
|
||||
let w = viewport.width;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制 AST、当前源码快照和云文件视图。
|
||||
//! 当前快照是发布源的只读投影;云文件 provider 的可用性与打开状态分别反馈,不把查看代码当成执行代码。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 原生宿主模块入口。窗口、管理控件、系统适配和开发工具状态与用户业务界面分开维护。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod activity;
|
||||
pub(crate) mod app;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! macOS 原生标题栏适配。定位系统窗口按钮并沿用用户的标题栏双击设置。
|
||||
//! 仅在 macOS 编译;Objective-C 交互局限在原生窗口层,不参与 Endor 布局与事件分发。
|
||||
//!
|
||||
//! Keep AppKit's real window buttons aligned with the custom tab strip.
|
||||
use objc2_app_kit::{NSApplication, NSView, NSWindowButton, NSWindowStyleMask};
|
||||
use objc2_foundation::{MainThreadMarker, NSPoint, NSString, NSUserDefaults};
|
||||
@@ -8,6 +11,7 @@ use winit::{
|
||||
|
||||
/// The custom draggable row does not receive AppKit's titlebar double-click
|
||||
/// action automatically. Use the native click count and native window actions.
|
||||
/// 读取系统偏好的标题栏双击行为,委托原生窗口执行相应动作。
|
||||
pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
let Some(main_thread) = MainThreadMarker::new() else {
|
||||
return false;
|
||||
@@ -49,6 +53,7 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
|
||||
/// Called only on winit's main thread, after native creation and before drawing.
|
||||
/// AppKit can restore its default button positions when laying out the title bar.
|
||||
/// 调整真实 macOS 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。
|
||||
pub fn align_controls(window: &Window) {
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 宿主管理工具和页面列表的唯一侧栏。展开时占据页面左侧空间,折叠后只保留展开按钮区域。
|
||||
//! 页面标题和身份来自发布帧;实际切换/关闭通过带版本的事件进入运行时,不在绘制时修改业务对象。
|
||||
//!
|
||||
//! The only persistent host toolbar. A collapsed sidebar owns just its toggle
|
||||
//! and the native traffic-light safe area; the page owns the rest of the window.
|
||||
use crate::{
|
||||
@@ -15,6 +18,7 @@ pub const HEADER: f32 = 40.0;
|
||||
pub const CONTROLS: f32 = if cfg!(target_os = "macos") { 86.0 } else { 0.0 };
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 侧栏释放激活后要执行的管理动作,绘制本身只登记它。
|
||||
pub enum Action {
|
||||
Toggle,
|
||||
NewPage,
|
||||
@@ -23,6 +27,7 @@ pub enum Action {
|
||||
Close(u64),
|
||||
}
|
||||
|
||||
/// 侧栏开合、控件交互和本帧目标集合,决定实际占用页面宽度。
|
||||
pub struct State {
|
||||
pub open: bool,
|
||||
pub interaction: Interaction,
|
||||
@@ -31,6 +36,7 @@ pub struct State {
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: true,
|
||||
@@ -42,10 +48,12 @@ impl Default for State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// 返回展开侧栏占据的逻辑宽度,折叠时页面收回这部分空间。
|
||||
pub fn left(&self) -> f32 {
|
||||
if self.open { WIDTH } else { 0.0 }
|
||||
}
|
||||
|
||||
/// 判断指针是否落入宿主侧栏/展开按钮区域,保护页面输入边界。
|
||||
pub fn covers(&self, point: Point) -> bool {
|
||||
if self.open {
|
||||
point.x >= 0.0 && point.x < WIDTH
|
||||
@@ -54,6 +62,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// 绘制工具入口、页面列表和展开控制,同时登记稳定的交互目标。
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
tabs: &[Tab],
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
//! 云文件浏览的 provider 抽象与工作区状态。默认 provider 未连接,不假装已有外部文件。
|
||||
//! 列出和打开文件的结果作为工具数据展示,错误保持在当前工具状态中供界面说明。
|
||||
//!
|
||||
//! Cloud source integration seam. The host installs a provider when one exists;
|
||||
//! the default workspace stays disconnected and never fabricates or stores files.
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// provider 提供的一条文件元数据,供云文件列表显示。
|
||||
pub struct SourceFile {
|
||||
pub id: String,
|
||||
pub path: String,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
/// 已经取得的源文件内容与标识,不代表它已经被执行。
|
||||
pub struct OpenedSource {
|
||||
pub path: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// 外部文件能力接口;只负责列出和读取,不与 VM 全局环境耦合。
|
||||
pub trait CloudSourceProvider: std::fmt::Debug + Send {
|
||||
/// 通过当前 provider 请求文件列表;默认未连接实现返回明确不可用状态。
|
||||
fn list_files(&mut self) -> Result<Vec<SourceFile>>;
|
||||
/// 通过 provider 取得指定文件内容,不在工具查看动作中执行该内容。
|
||||
fn open_file(&mut self, id: &str) -> Result<OpenedSource>;
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
/// Sources 云文件页面保留的 provider、列表、选择、内容与错误状态。
|
||||
pub struct SourceWorkspace {
|
||||
pub provider: Option<Box<dyn CloudSourceProvider>>,
|
||||
pub files: Vec<SourceFile>,
|
||||
@@ -25,6 +34,7 @@ pub struct SourceWorkspace {
|
||||
pub error: String,
|
||||
}
|
||||
impl SourceWorkspace {
|
||||
/// 更新云文件列表并保存结果或错误,供 Sources 页面显示。
|
||||
pub fn refresh(&mut self) {
|
||||
let Some(provider) = &mut self.provider else {
|
||||
self.error = "No cloud file provider configured".into();
|
||||
@@ -38,6 +48,7 @@ impl SourceWorkspace {
|
||||
Err(error) => self.error = error.to_string(),
|
||||
}
|
||||
}
|
||||
/// 更新选中文件的内容与打开状态,失败保持可读错误信息。
|
||||
pub fn open(&mut self, id: &str) {
|
||||
let Some(provider) = &mut self.provider else {
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! 即时绘制控件的跨帧交互状态:稳定 WidgetId、悬停、按下捕获、键盘焦点和释放激活。
|
||||
//! 控件用语义 key 标识,而不是屏幕位置或行号。释放时必须仍命中同一个启用目标,拖出控件不能意外触发点击。
|
||||
//!
|
||||
use crate::geometry::{Point, Rect};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Stable across frames, resizing and text changes. Use semantic keys, never row indices.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 由语义 key 生成的稳定控件身份,跨帧移动和文字变化不应改变它。
|
||||
pub struct WidgetId(u64);
|
||||
impl WidgetId {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(key: impl Hash) -> Self {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
key.hash(&mut hash);
|
||||
@@ -12,11 +17,13 @@ impl WidgetId {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 分隔条和滚动条的操作方向。
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 管理控件角色,决定焦点资格和系统光标。
|
||||
pub enum Role {
|
||||
Button,
|
||||
Tab,
|
||||
@@ -28,11 +35,13 @@ pub enum Role {
|
||||
Scrollbar(Axis),
|
||||
}
|
||||
impl Role {
|
||||
/// 判断控件角色是否可以接受键盘焦点。
|
||||
fn focusable(self) -> bool {
|
||||
!matches!(self, Self::Splitter(_) | Self::Scrollbar(_) | Self::Row)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 宿主控件请求的系统光标种类。
|
||||
pub enum Cursor {
|
||||
Default,
|
||||
Pointer,
|
||||
@@ -43,6 +52,7 @@ pub enum Cursor {
|
||||
Disabled,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
/// 本帧可命中的管理控件,绑定几何、角色、启用条件与可选 action。
|
||||
pub struct Target<A> {
|
||||
pub id: WidgetId,
|
||||
pub rect: Rect,
|
||||
@@ -51,6 +61,7 @@ pub struct Target<A> {
|
||||
pub action: Option<A>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 绘制使用的组合交互状态,避免按钮自己重新推断焦点/捕获。
|
||||
pub struct VisualState {
|
||||
pub hovered: bool,
|
||||
pub pressed: bool,
|
||||
@@ -62,6 +73,7 @@ pub struct VisualState {
|
||||
/// Retained input state for an immediate-mode frame. Actions fire on release inside
|
||||
/// the same enabled control, so dragging away cancels a click.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 即时绘制之外保留的输入状态,确保按下与释放跨帧仍匹配同一控件。
|
||||
pub struct Interaction {
|
||||
pointer: Option<Point>,
|
||||
hovered: Option<WidgetId>,
|
||||
@@ -71,18 +83,22 @@ pub struct Interaction {
|
||||
keyboard_focus: bool,
|
||||
}
|
||||
impl Interaction {
|
||||
/// 按逆绘制顺序选择指针处的管理控件,避免重叠控件同时激活。
|
||||
pub fn hit<'a, A>(&self, targets: &'a [Target<A>]) -> Option<&'a Target<A>> {
|
||||
let p = self.pointer?;
|
||||
targets.iter().rev().find(|t| t.rect.contains(p))
|
||||
}
|
||||
/// 更新指针与悬停目标,但不在移动时直接触发点击。
|
||||
pub fn pointer_moved<A>(&mut self, p: Point, targets: &[Target<A>]) {
|
||||
self.pointer = Some(p);
|
||||
self.hovered = self.hit(targets).map(|t| t.id);
|
||||
}
|
||||
/// 清除离开窗口后的指针/悬停状态。
|
||||
pub fn pointer_left(&mut self) {
|
||||
self.pointer = None;
|
||||
self.hovered = None;
|
||||
}
|
||||
/// 捕获启用控件的稳定身份,并按角色更新键盘焦点。
|
||||
pub fn press<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<Target<A>> {
|
||||
self.key_active = None;
|
||||
self.keyboard_focus = false;
|
||||
@@ -91,22 +107,26 @@ impl Interaction {
|
||||
self.focused = target.as_ref().filter(|t| t.role.focusable()).map(|t| t.id);
|
||||
target
|
||||
}
|
||||
/// 只有释放时仍处于原启用目标内才返回 action,拖出后释放不触发。
|
||||
pub fn release<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
||||
let active = self.pointer_active.take()?;
|
||||
self.hit(targets)
|
||||
.filter(|t| t.enabled && t.id == active && t.role != Role::TextField)
|
||||
.and_then(|t| t.action.clone())
|
||||
}
|
||||
/// 撤销当前操作的后续交付资格,并执行对应取消清理。
|
||||
pub fn cancel(&mut self) {
|
||||
self.pointer_active = None;
|
||||
self.key_active = None;
|
||||
}
|
||||
/// 撤销失焦后的键盘/指针激活状态,防止回到窗口时补发旧操作。
|
||||
pub fn blur(&mut self) {
|
||||
self.cancel();
|
||||
self.focused = None;
|
||||
self.keyboard_focus = false;
|
||||
self.pointer_left();
|
||||
}
|
||||
/// 用本帧目标集合校正跨帧焦点和捕获,已消失控件不保留可激活身份。
|
||||
pub fn reconcile<A>(&mut self, targets: &[Target<A>]) -> bool {
|
||||
let before = (
|
||||
self.hovered,
|
||||
@@ -127,11 +147,13 @@ impl Interaction {
|
||||
self.key_active,
|
||||
)
|
||||
}
|
||||
/// 返回当前仍持有焦点的稳定控件身份。
|
||||
pub fn focused<'a, A>(&self, targets: &'a [Target<A>]) -> Option<&'a Target<A>> {
|
||||
targets
|
||||
.iter()
|
||||
.find(|t| Some(t.id) == self.focused && t.enabled)
|
||||
}
|
||||
/// 沿启用且可聚焦的目标循环导航,支持正向与反向遍历。
|
||||
pub fn focus_next<A: Clone>(&mut self, targets: &[Target<A>], back: bool) -> Option<Target<A>> {
|
||||
self.cancel();
|
||||
self.keyboard_focus = true;
|
||||
@@ -154,18 +176,21 @@ impl Interaction {
|
||||
self.focused = Some(target.id);
|
||||
Some(target.clone())
|
||||
}
|
||||
/// 为当前焦点控件记录键盘激活,后续释放仍须匹配目标。
|
||||
pub fn key_press<A>(&mut self, targets: &[Target<A>]) {
|
||||
self.key_active = self
|
||||
.focused(targets)
|
||||
.filter(|t| t.role != Role::TextField)
|
||||
.map(|t| t.id);
|
||||
}
|
||||
/// 结束键盘激活并按有效控件身份返回操作。
|
||||
pub fn key_release<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
||||
let active = self.key_active.take()?;
|
||||
self.focused(targets)
|
||||
.filter(|t| t.id == active)
|
||||
.and_then(|t| t.action.clone())
|
||||
}
|
||||
/// 组合悬停、按下、焦点、选中和启用状态,供控件选择正确样式。
|
||||
pub fn visual(&self, id: WidgetId, selected: bool, enabled: bool) -> VisualState {
|
||||
let hovered = enabled && self.hovered == Some(id);
|
||||
VisualState {
|
||||
@@ -177,9 +202,11 @@ impl Interaction {
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
/// 返回本次拖动/按压捕获的控件,移动过程中不随悬停目标跳变。
|
||||
pub fn captured(&self, id: WidgetId) -> bool {
|
||||
self.pointer_active == Some(id)
|
||||
}
|
||||
/// 按命中角色选择系统光标,例如文本、分隔条或可点击按钮。
|
||||
pub fn cursor<A>(&self, targets: &[Target<A>]) -> Cursor {
|
||||
let target = self
|
||||
.pointer_active
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 宿主管理控件的共享组件入口。主题、绘制原语和跨帧交互状态分离,用户 Endor 页面不依赖这里的控件树。
|
||||
//!
|
||||
//! Tessera's host management widgets. No Endor AST, VM, UiNode or ui_bridge dependency.
|
||||
//! Shares only low-level drawing, font shaping and geometry with guest content.
|
||||
mod interaction;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 宿主控件的颜色与状态调色。根据背景、强调色和交互状态生成可读颜色,不改变业务状态。
|
||||
//!
|
||||
use crate::display_list::Color;
|
||||
pub const BG: Color = Color::rgb(255, 255, 255);
|
||||
// A visibly separate chrome layer against the warm, near-white content canvas.
|
||||
@@ -16,25 +18,31 @@ pub const PALETTE: [Color; 6] = [
|
||||
Color::rgb(52, 123, 78),
|
||||
];
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 当前管理界面的配色基础,派生状态颜色由方法统一计算。
|
||||
pub struct Theme {
|
||||
pub accent: Color,
|
||||
}
|
||||
impl Theme {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(accent: Color) -> Self {
|
||||
Self {
|
||||
accent: readable_accent(accent),
|
||||
}
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn soft(self) -> Color {
|
||||
mix(BG, self.accent, 0.10)
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn hover(self) -> Color {
|
||||
mix(BG, self.accent, 0.17)
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn pressed(self) -> Color {
|
||||
mix(BG, self.accent, 0.29)
|
||||
}
|
||||
}
|
||||
/// 按比例混合颜色通道,作为状态颜色的基础运算。
|
||||
pub fn mix(a: Color, b: Color, amount: f32) -> Color {
|
||||
let channel =
|
||||
|a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * amount.clamp(0.0, 1.0)).round() as u8;
|
||||
@@ -44,6 +52,7 @@ pub fn mix(a: Color, b: Color, amount: f32) -> Color {
|
||||
channel(a.blue, b.blue),
|
||||
)
|
||||
}
|
||||
/// 根据背景选择可读的强调色,供选中与焦点状态使用。
|
||||
pub fn readable_accent(mut c: Color) -> Color {
|
||||
while (c.red as u32 * 299 + c.green as u32 * 587 + c.blue as u32 * 114) / 1000 > 155 {
|
||||
c = Color::rgb(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 用统一主题绘制宿主管理控件,并同步注册交互目标。
|
||||
//! 每个控件的图形、角色、启用状态和 action 一起登记,绘制几何与命中几何保持一致。
|
||||
//!
|
||||
use super::{
|
||||
Axis, Interaction, Role, Target, VisualState, WidgetId,
|
||||
theme::{self, Theme},
|
||||
@@ -9,6 +12,7 @@ use crate::{
|
||||
};
|
||||
use std::hash::Hash;
|
||||
|
||||
/// 一次按钮绘制的借用描述,包括显示、角色和启用/选中状态。
|
||||
pub struct Button<'a> {
|
||||
pub label: &'a str,
|
||||
pub selected: bool,
|
||||
@@ -16,6 +20,7 @@ pub struct Button<'a> {
|
||||
pub role: Role,
|
||||
}
|
||||
impl<'a> Button<'a> {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(label: &'a str) -> Self {
|
||||
Self {
|
||||
label,
|
||||
@@ -24,20 +29,24 @@ impl<'a> Button<'a> {
|
||||
role: Role::Button,
|
||||
}
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn selected(mut self, value: bool) -> Self {
|
||||
self.selected = value;
|
||||
self
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn enabled(mut self, value: bool) -> Self {
|
||||
self.enabled = value;
|
||||
self
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn role(mut self, value: Role) -> Self {
|
||||
self.role = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 本帧宿主控件绘制上下文,共同拥有显示列表与交互目标登记入口。
|
||||
pub struct Ui<A> {
|
||||
pub list: DisplayList,
|
||||
pub targets: Vec<Target<A>>,
|
||||
@@ -45,6 +54,7 @@ pub struct Ui<A> {
|
||||
interaction: Interaction,
|
||||
}
|
||||
impl<A: Hash> Ui<A> {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(theme: Theme, interaction: Interaction) -> Self {
|
||||
Self {
|
||||
list: DisplayList::default(),
|
||||
@@ -53,6 +63,7 @@ impl<A: Hash> Ui<A> {
|
||||
interaction,
|
||||
}
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
pub fn fill(&mut self, r: Rect, color: Color) {
|
||||
self.list.push(DrawCommand::FillRoundedRect {
|
||||
rect: r,
|
||||
@@ -60,6 +71,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
fn outline(&mut self, r: Rect, color: Color, width: f32) {
|
||||
self.list.push(DrawCommand::StrokeRoundedRect {
|
||||
rect: Rect::new(
|
||||
@@ -73,6 +85,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
pub fn label(&mut self, x: f32, y: f32, value: &str, color: Color, code: bool) {
|
||||
self.list.push(DrawCommand::Text {
|
||||
origin: Point::new(x, y),
|
||||
@@ -89,6 +102,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 把控件身份、几何、角色、启用状态和 action 一起加入本帧交互目标。
|
||||
fn register(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
@@ -108,6 +122,7 @@ impl<A: Hash> Ui<A> {
|
||||
});
|
||||
response
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
fn surface(&mut self, rect: Rect, s: VisualState, base: Option<Color>) {
|
||||
let color = if !s.enabled {
|
||||
base
|
||||
@@ -127,6 +142,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.outline(rect, self.theme.accent, 1.5);
|
||||
}
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn button(&mut self, rect: Rect, action: A, button: Button<'_>) -> VisualState {
|
||||
let id = WidgetId::new(&action);
|
||||
let s = self.register(
|
||||
@@ -168,6 +184,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.list.push(DrawCommand::PopClip);
|
||||
s
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn text_field(&mut self, rect: Rect, action: A, focused: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -191,6 +208,7 @@ impl<A: Hash> Ui<A> {
|
||||
);
|
||||
}
|
||||
/// A terminal prompt shares its surface with output; its caret is the focus cue.
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn inline_input(&mut self, rect: Rect, action: A) {
|
||||
self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -201,6 +219,7 @@ impl<A: Hash> Ui<A> {
|
||||
true,
|
||||
);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn row(&mut self, paint: Rect, hit: Rect, action: A, selected: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -212,6 +231,7 @@ impl<A: Hash> Ui<A> {
|
||||
);
|
||||
self.surface(paint, s, None);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn swatch(&mut self, rect: Rect, action: A, color: Color, selected: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -249,6 +269,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.label(rect.x + 10.0, rect.y + 6.0, "✓", theme::BG, false);
|
||||
}
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn splitter(&mut self, rect: Rect, id: WidgetId, axis: Axis) {
|
||||
let hit = Rect::new(
|
||||
rect.x - 3.0,
|
||||
@@ -273,6 +294,7 @@ impl<A: Hash> Ui<A> {
|
||||
};
|
||||
self.fill(line, color);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn scrollbar(&mut self, rect: Rect, id: WidgetId, axis: Axis) {
|
||||
let mut s = self.register(id, rect, Role::Scrollbar(axis), None, false, true);
|
||||
s.pressed |= self.interaction.captured(id);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! 发布页面标签的数据结构。保留稳定页面身份、标题、选中状态和开发工具类别;页面的托管所有权由模型层管理。
|
||||
//!
|
||||
//! Immutable page registry projections. Host navigation is rendered in sidebar.
|
||||
//! IDs are session-local data and never own managed VM objects.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// 托管页面注册表的不可变投影,原生工具只使用稳定 ID 与显示元数据。
|
||||
pub struct Tab {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
|
||||
Reference in New Issue
Block a user