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

163 lines
5.2 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)));
}
}