//! 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>, 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))); } }