| 1 | //! Dockable panes: Keil's edit and debug window layouts on `egui_tiles`, |
| 2 | //! with Nothing-style tab bars and flat surface cards. |
| 3 | |
| 4 | use eframe::egui::{ |
| 5 | self, Align2, Color32, CornerRadius, Id, Rect, Response, Sense, Shape, Stroke, Ui, Vec2, |
| 6 | WidgetText, vec2, |
| 7 | }; |
| 8 | use egui_tiles::{ |
| 9 | Behavior, Container, LinearDir, ResizeState, SimplificationOptions, TabState, Tabs, Tile, |
| 10 | TileId, Tiles, Tree, UiResponse, |
| 11 | }; |
| 12 | use serde::{Deserialize, Serialize}; |
| 13 | |
| 14 | use crate::panes; |
| 15 | use crate::state::State; |
| 16 | use crate::theme::{self, palette}; |
| 17 | |
| 18 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 19 | pub enum Pane { |
| 20 | Project, |
| 21 | Editor, |
| 22 | Build, |
| 23 | Registers, |
| 24 | Memory, |
| 25 | Disassembly, |
| 26 | Symbols, |
| 27 | Serial, |
| 28 | Ports, |
| 29 | Timers, |
| 30 | Interrupts, |
| 31 | } |
| 32 | |
| 33 | impl Pane { |
| 34 | pub const ALL: [Pane; 11] = [ |
| 35 | Pane::Project, |
| 36 | Pane::Editor, |
| 37 | Pane::Build, |
| 38 | Pane::Registers, |
| 39 | Pane::Disassembly, |
| 40 | Pane::Memory, |
| 41 | Pane::Symbols, |
| 42 | Pane::Serial, |
| 43 | Pane::Ports, |
| 44 | Pane::Timers, |
| 45 | Pane::Interrupts, |
| 46 | ]; |
| 47 | |
| 48 | pub fn title(self) -> &'static str { |
| 49 | match self { |
| 50 | Pane::Project => "Project", |
| 51 | Pane::Editor => "Editor", |
| 52 | Pane::Build => "Build Output", |
| 53 | Pane::Registers => "Registers", |
| 54 | Pane::Memory => "Memory", |
| 55 | Pane::Disassembly => "Disassembly", |
| 56 | Pane::Symbols => "Symbols", |
| 57 | Pane::Serial => "UART #1", |
| 58 | Pane::Ports => "Ports", |
| 59 | Pane::Timers => "Timers", |
| 60 | Pane::Interrupts => "Interrupts", |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /// Panes this one is grouped with by default, to dock it next to them |
| 65 | /// when it is reopened. |
| 66 | fn neighbours(self) -> &'static [Pane] { |
| 67 | match self { |
| 68 | Pane::Project | Pane::Registers => &[Pane::Registers, Pane::Project], |
| 69 | Pane::Editor | Pane::Disassembly => &[Pane::Disassembly, Pane::Editor], |
| 70 | Pane::Build | Pane::Memory | Pane::Symbols => { |
| 71 | &[Pane::Memory, Pane::Build, Pane::Symbols] |
| 72 | } |
| 73 | Pane::Serial | Pane::Ports | Pane::Timers | Pane::Interrupts => &[ |
| 74 | Pane::Serial, |
| 75 | Pane::Ports, |
| 76 | Pane::Timers, |
| 77 | Pane::Interrupts, |
| 78 | Pane::Memory, |
| 79 | ], |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | const GAP: f32 = 6.0; |
| 85 | const TAB_BAR: f32 = 30.0; |
| 86 | |
| 87 | fn tabs(tiles: &mut Tiles<Pane>, panes: &[Pane]) -> TileId { |
| 88 | let ids = panes.iter().map(|&p| tiles.insert_pane(p)).collect(); |
| 89 | tiles.insert_tab_tile(ids) |
| 90 | } |
| 91 | |
| 92 | fn linear(tiles: &mut Tiles<Pane>, dir: LinearDir, children: &[(TileId, f32)]) -> TileId { |
| 93 | let ids: Vec<TileId> = children.iter().map(|c| c.0).collect(); |
| 94 | let id = match dir { |
| 95 | LinearDir::Horizontal => tiles.insert_horizontal_tile(ids), |
| 96 | LinearDir::Vertical => tiles.insert_vertical_tile(ids), |
| 97 | }; |
| 98 | if let Some(Tile::Container(Container::Linear(l))) = tiles.get_mut(id) { |
| 99 | for &(child, share) in children { |
| 100 | l.shares.set_share(child, share); |
| 101 | } |
| 102 | } |
| 103 | id |
| 104 | } |
| 105 | |
| 106 | /// Keil's build-mode layout: project on the left, editor over build output. |
| 107 | pub fn edit_tree() -> Tree<Pane> { |
| 108 | let mut t = Tiles::default(); |
| 109 | let project = tabs(&mut t, &[Pane::Project]); |
| 110 | let editor = tabs(&mut t, &[Pane::Editor]); |
| 111 | let build = tabs(&mut t, &[Pane::Build]); |
| 112 | let right = linear(&mut t, LinearDir::Vertical, &[(editor, 0.7), (build, 0.3)]); |
| 113 | let root = linear( |
| 114 | &mut t, |
| 115 | LinearDir::Horizontal, |
| 116 | &[(project, 0.18), (right, 0.82)], |
| 117 | ); |
| 118 | Tree::new(Id::unique("edit_layout"), root, t) |
| 119 | } |
| 120 | |
| 121 | /// Keil's debug-mode layout: registers on the left, source and disassembly |
| 122 | /// in the middle, memory and peripherals along the bottom. |
| 123 | pub fn debug_tree() -> Tree<Pane> { |
| 124 | let mut t = Tiles::default(); |
| 125 | let regs = tabs(&mut t, &[Pane::Registers, Pane::Project]); |
| 126 | let editor = tabs(&mut t, &[Pane::Editor]); |
| 127 | let disasm = tabs(&mut t, &[Pane::Disassembly]); |
| 128 | let mem = tabs(&mut t, &[Pane::Memory, Pane::Symbols, Pane::Build]); |
| 129 | let periph = tabs( |
| 130 | &mut t, |
| 131 | &[Pane::Serial, Pane::Ports, Pane::Timers, Pane::Interrupts], |
| 132 | ); |
| 133 | let top = linear( |
| 134 | &mut t, |
| 135 | LinearDir::Horizontal, |
| 136 | &[(editor, 0.62), (disasm, 0.38)], |
| 137 | ); |
| 138 | let bottom = linear( |
| 139 | &mut t, |
| 140 | LinearDir::Horizontal, |
| 141 | &[(mem, 0.56), (periph, 0.44)], |
| 142 | ); |
| 143 | let right = linear(&mut t, LinearDir::Vertical, &[(top, 0.6), (bottom, 0.4)]); |
| 144 | let root = linear( |
| 145 | &mut t, |
| 146 | LinearDir::Horizontal, |
| 147 | &[(regs, 0.19), (right, 0.81)], |
| 148 | ); |
| 149 | Tree::new(Id::unique("debug_layout"), root, t) |
| 150 | } |
| 151 | |
| 152 | /// Brings `pane` to the front, docking it next to its usual neighbours if |
| 153 | /// it was closed. |
| 154 | pub fn show_pane(tree: &mut Tree<Pane>, pane: Pane) { |
| 155 | if let Some(id) = tree.tiles.find_pane(&pane) { |
| 156 | tree.make_active(|tid, _| tid == id); |
| 157 | return; |
| 158 | } |
| 159 | let new = tree.tiles.insert_pane(pane); |
| 160 | let target = pane |
| 161 | .neighbours() |
| 162 | .iter() |
| 163 | .filter_map(|n| tree.tiles.find_pane(n)) |
| 164 | .filter_map(|id| tree.tiles.parent_of(id)) |
| 165 | .find(|&parent| { |
| 166 | matches!( |
| 167 | tree.tiles.get(parent), |
| 168 | Some(Tile::Container(Container::Tabs(_))) |
| 169 | ) |
| 170 | }); |
| 171 | if let Some(parent) = target |
| 172 | && let Some(Tile::Container(Container::Tabs(t))) = tree.tiles.get_mut(parent) |
| 173 | { |
| 174 | t.add_child(new); |
| 175 | t.set_active(new); |
| 176 | return; |
| 177 | } |
| 178 | let tabs_id = tree.tiles.insert_container(Tabs::new(vec![new])); |
| 179 | match tree.root { |
| 180 | Some(root) => { |
| 181 | if let Some(Tile::Container(c)) = tree.tiles.get_mut(root) { |
| 182 | c.add_child(tabs_id); |
| 183 | } else { |
| 184 | tree.root = Some(tree.tiles.insert_horizontal_tile(vec![root, tabs_id])); |
| 185 | } |
| 186 | } |
| 187 | None => tree.root = Some(tabs_id), |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | /// Flat surface cards behind every visible tab group: the page shows |
| 192 | /// through the gaps between them. |
| 193 | pub fn card_shapes(tree: &Tree<Pane>, fill: Color32) -> Vec<Shape> { |
| 194 | tree.tiles |
| 195 | .iter() |
| 196 | .filter(|(_, t)| matches!(t, Tile::Container(Container::Tabs(_)))) |
| 197 | .filter_map(|(&id, _)| tree.tiles.rect(id)) |
| 198 | .map(|r| Shape::rect_filled(r, CornerRadius::same(8), fill)) |
| 199 | .collect() |
| 200 | } |
| 201 | |
| 202 | pub struct PaneBehavior<'a> { |
| 203 | pub st: &'a mut State, |
| 204 | /// Tile of the editor pane, whose group hides its tab bar. |
| 205 | pub editor_tile: Option<TileId>, |
| 206 | } |
| 207 | |
| 208 | impl Behavior<Pane> for PaneBehavior<'_> { |
| 209 | fn pane_ui(&mut self, ui: &mut Ui, _tile_id: TileId, pane: &mut Pane) -> UiResponse { |
| 210 | panes::show(*pane, ui, self.st); |
| 211 | UiResponse::None |
| 212 | } |
| 213 | |
| 214 | fn tab_title_for_pane(&mut self, pane: &Pane) -> WidgetText { |
| 215 | pane.title().into() |
| 216 | } |
| 217 | |
| 218 | fn is_tab_closable(&self, tiles: &Tiles<Pane>, tile_id: TileId) -> bool { |
| 219 | matches!(tiles.get(tile_id), Some(Tile::Pane(p)) if *p != Pane::Editor) |
| 220 | } |
| 221 | |
| 222 | fn tab_ui( |
| 223 | &mut self, |
| 224 | tiles: &mut Tiles<Pane>, |
| 225 | ui: &mut Ui, |
| 226 | id: Id, |
| 227 | tile_id: TileId, |
| 228 | state: &TabState, |
| 229 | ) -> Response { |
| 230 | let p = palette(ui.ctx()); |
| 231 | let title = match tiles.get(tile_id) { |
| 232 | Some(Tile::Pane(pane)) => pane.title(), |
| 233 | _ => "Group", |
| 234 | }; |
| 235 | let galley = WidgetText::from(p.label(title)).into_galley( |
| 236 | ui, |
| 237 | Some(egui::TextWrapMode::Extend), |
| 238 | f32::INFINITY, |
| 239 | egui::TextStyle::Small, |
| 240 | ); |
| 241 | let pad = 12.0; |
| 242 | let close_w = if state.closable { 16.0 } else { 0.0 }; |
| 243 | let width = galley.size().x + 2.0 * pad + close_w; |
| 244 | let (_, rect) = ui.allocate_space(vec2(width, ui.available_height())); |
| 245 | let resp = ui |
| 246 | .interact(rect, id, Sense::click_and_drag()) |
| 247 | .on_hover_cursor(egui::CursorIcon::PointingHand); |
| 248 | |
| 249 | if ui.is_rect_visible(rect) && !state.is_being_dragged { |
| 250 | let fg = if state.active { |
| 251 | p.text_display |
| 252 | } else if resp.hovered() { |
| 253 | p.text_secondary |
| 254 | } else { |
| 255 | p.text_disabled |
| 256 | }; |
| 257 | let text_rect = Rect::from_min_max( |
| 258 | rect.min + vec2(pad, 0.0), |
| 259 | rect.max - vec2(pad + close_w, 0.0), |
| 260 | ); |
| 261 | let pos = Align2::LEFT_CENTER |
| 262 | .align_size_within_rect(galley.size(), text_rect) |
| 263 | .min; |
| 264 | let text_w = galley.size().x; |
| 265 | ui.painter().galley(pos, galley, fg); |
| 266 | if state.active { |
| 267 | ui.painter().hline( |
| 268 | pos.x..=pos.x + text_w, |
| 269 | rect.bottom() - 1.0, |
| 270 | Stroke::new(2.0, p.text_display), |
| 271 | ); |
| 272 | } |
| 273 | if state.closable { |
| 274 | let c = Rect::from_center_size( |
| 275 | egui::pos2(rect.right() - pad - 4.0, rect.center().y), |
| 276 | Vec2::splat(12.0), |
| 277 | ); |
| 278 | let close = ui |
| 279 | .interact(c, id.with("close"), Sense::click()) |
| 280 | .on_hover_cursor(egui::CursorIcon::PointingHand); |
| 281 | if resp.hovered() || close.hovered() || state.active { |
| 282 | let col = if close.hovered() { |
| 283 | p.text_display |
| 284 | } else { |
| 285 | p.text_disabled |
| 286 | }; |
| 287 | let r = c.shrink(3.0); |
| 288 | let s = Stroke::new(1.2, col); |
| 289 | ui.painter() |
| 290 | .line_segment([r.left_top(), r.right_bottom()], s); |
| 291 | ui.painter() |
| 292 | .line_segment([r.right_top(), r.left_bottom()], s); |
| 293 | } |
| 294 | if (close.clicked() || resp.clicked_by(egui::PointerButton::Middle)) |
| 295 | && self.on_tab_close(tiles, tile_id) |
| 296 | { |
| 297 | tiles.remove(tile_id); |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | resp |
| 302 | } |
| 303 | |
| 304 | fn is_tab_bar_visible(&self, _tile_id: TileId, tabs: &Tabs) -> bool { |
| 305 | // The editor brings its own document tabs; a second bar above them |
| 306 | // would only repeat "EDITOR". |
| 307 | !(tabs.children.len() == 1 && self.editor_tile == Some(tabs.children[0])) |
| 308 | } |
| 309 | |
| 310 | fn tab_bar_height(&self, _style: &egui::Style) -> f32 { |
| 311 | TAB_BAR |
| 312 | } |
| 313 | |
| 314 | fn gap_width(&self, _style: &egui::Style) -> f32 { |
| 315 | GAP |
| 316 | } |
| 317 | |
| 318 | fn min_size(&self) -> f32 { |
| 319 | 64.0 |
| 320 | } |
| 321 | |
| 322 | fn tab_bar_color(&self, _visuals: &egui::Visuals) -> Color32 { |
| 323 | Color32::TRANSPARENT |
| 324 | } |
| 325 | |
| 326 | fn tab_bar_hline_stroke(&self, visuals: &egui::Visuals) -> Stroke { |
| 327 | let p = if visuals.dark_mode { |
| 328 | &theme::DARK |
| 329 | } else { |
| 330 | &theme::LIGHT |
| 331 | }; |
| 332 | Stroke::new(1.0, p.border) |
| 333 | } |
| 334 | |
| 335 | fn resize_stroke(&self, style: &egui::Style, state: ResizeState) -> Stroke { |
| 336 | let p = if style.visuals.dark_mode { |
| 337 | &theme::DARK |
| 338 | } else { |
| 339 | &theme::LIGHT |
| 340 | }; |
| 341 | match state { |
| 342 | ResizeState::Idle => Stroke::NONE, |
| 343 | ResizeState::Hovering => Stroke::new(1.0, p.text_disabled), |
| 344 | ResizeState::Dragging => Stroke::new(1.0, p.text_display), |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | fn simplification_options(&self) -> SimplificationOptions { |
| 349 | SimplificationOptions { |
| 350 | all_panes_must_have_tabs: true, |
| 351 | ..Default::default() |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | fn drag_preview_color(&self, visuals: &egui::Visuals) -> Color32 { |
| 356 | visuals.selection.bg_fill |
| 357 | } |
| 358 | |
| 359 | fn drag_preview_stroke(&self, visuals: &egui::Visuals) -> Stroke { |
| 360 | visuals.selection.stroke |
| 361 | } |
| 362 | } |