Files
endorphin-tessera-gui/src/shell/sidebar.rs
T

172 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 宿主管理工具和页面列表的唯一侧栏。展开时占据页面左侧空间,折叠后只保留展开按钮区域。
//! 页面标题和身份来自发布帧;实际切换/关闭通过带版本的事件进入运行时,不在绘制时修改业务对象。
//!
//! 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::{
display_list::{DisplayList, DrawCommand},
geometry::{Point, Rect},
system_ui::{
Button, Interaction, Target, Ui,
theme::{self, Theme},
},
tabs::Tab,
};
pub const WIDTH: f32 = 256.0;
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,
Tool(&'static str),
Activate(u64),
Close(u64),
}
/// 侧栏开合、控件交互和本帧目标集合,决定实际占用页面宽度。
pub struct State {
pub open: bool,
pub interaction: Interaction,
pub targets: Vec<Target<Action>>,
pub scroll: f32,
}
impl Default for State {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
open: true,
interaction: Interaction::default(),
targets: Vec::new(),
scroll: 0.0,
}
}
}
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
} else {
Rect::new(0.0, 0.0, CONTROLS + 44.0, HEADER).contains(point)
}
}
/// 绘制工具入口、页面列表和展开控制,同时登记稳定的交互目标。
pub fn draw(
&mut self,
tabs: &[Tab],
height: f32,
accent: crate::display_list::Color,
) -> DisplayList {
let mut ui = Ui::new(Theme::new(accent), self.interaction.clone());
if self.open {
ui.fill(Rect::new(0.0, 0.0, WIDTH, height), theme::BAR);
}
let toggle_x = if self.open {
WIDTH - 40.0
} else {
CONTROLS + 4.0
};
// Draw a sidebar icon with primitives so its shape does not depend on fonts.
ui.button(
Rect::new(toggle_x, 6.0, 30.0, 28.0),
Action::Toggle,
Button::new(""),
);
ui.list.push(DrawCommand::StrokeRoundedRect {
rect: Rect::new(toggle_x + 7.0, 13.0, 16.0, 13.0),
radius: 2.0,
width: 1.0,
color: theme::MUTED,
});
ui.fill(Rect::new(toggle_x + 12.0, 14.0, 1.0, 11.0), theme::MUTED);
if self.open {
ui.label(20.0, 61.0, "ENDORPHIN", theme::TEXT, false);
ui.button(
Rect::new(12.0, 95.0, WIDTH - 24.0, 34.0),
Action::NewPage,
Button::new("+ 新建页面"),
);
for (index, (name, label)) in [
("Console", "Console"),
("Sources", "Sources"),
("Network", "Network"),
("Settings", "Settings"),
]
.into_iter()
.enumerate()
{
let selected = tabs
.iter()
.any(|tab| tab.active && tab.developer_tool.as_deref() == Some(name));
ui.button(
Rect::new(12.0, 147.0 + index as f32 * 34.0, WIDTH - 24.0, 30.0),
Action::Tool(name),
Button::new(label).selected(selected),
);
}
ui.label(20.0, 303.0, "页面", theme::MUTED, false);
let area = Rect::new(8.0, 333.0, WIDTH - 16.0, (height - 383.0).max(0.0));
let pages: Vec<_> = tabs
.iter()
.filter(|tab| tab.developer_tool.is_none())
.collect();
self.scroll = self
.scroll
.clamp(0.0, (pages.len() as f32 * 36.0 - area.height).max(0.0));
ui.list.push(DrawCommand::PushClip(area));
for (index, tab) in pages.into_iter().enumerate() {
let y = area.y + index as f32 * 36.0 - self.scroll;
// Register only fully visible rows: paint clipping must match input.
if y < area.y || y + 32.0 > area.y + area.height {
continue;
}
ui.button(
Rect::new(12.0, y, WIDTH - 56.0, 32.0),
Action::Activate(tab.id),
Button::new(&tab.title).selected(tab.active),
);
ui.button(
Rect::new(WIDTH - 40.0, y, 28.0, 32.0),
Action::Close(tab.id),
Button::new("×"),
);
}
ui.list.push(DrawCommand::PopClip);
ui.label(20.0, height - 32.0, "本地工作区", theme::MUTED, false);
}
self.targets = ui.targets;
self.interaction.reconcile(&self.targets);
ui.list
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collapsing_removes_every_host_target_except_the_toggle() {
let mut sidebar = State::default();
sidebar.draw(&[], 800.0, theme::PALETTE[0]);
assert!(sidebar.targets.len() > 1);
sidebar.open = false;
sidebar.draw(&[], 800.0, theme::PALETTE[0]);
assert_eq!(sidebar.targets.len(), 1);
assert_eq!(sidebar.targets[0].action, Some(Action::Toggle));
assert_eq!(sidebar.left(), 0.0);
assert!(!sidebar.covers(Point::new(400.0, 10.0)));
assert!(!sidebar.covers(Point::new(10.0, 80.0)));
}
}