1use crate::atoms::icons;
32use crate::Theme;
33use egui::{
34 emath::TSTransform, Color32, CornerRadius, FontFamily, Pos2, Rect, Scene, Sense, Stroke, Ui,
35 Vec2,
36};
37use std::collections::HashMap;
38
39#[derive(Clone, Debug)]
41pub struct LayoutPane {
42 pub id: String,
44 pub title: String,
46 pub title_icon: Option<&'static str>,
48 pub position: Pos2,
50 pub size: Vec2,
52 pub pre_maximize_size: Option<Vec2>,
54 pub pre_maximize_position: Option<Pos2>,
56 pub closable: bool,
58 pub collapsed: bool,
60 pub maximized: bool,
62 pub resizable: bool,
64 pub min_size: Vec2,
66 pub lock_level: LockLevel,
68}
69
70impl LayoutPane {
71 pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
73 Self {
74 id: id.into(),
75 title: title.into(),
76 title_icon: None,
77 position: Pos2::ZERO,
78 size: Vec2::new(300.0, 200.0),
79 pre_maximize_size: None,
80 pre_maximize_position: None,
81 closable: false,
82 collapsed: false,
83 maximized: false,
84 resizable: true,
85 min_size: Vec2::new(100.0, 60.0),
86 lock_level: LockLevel::None,
87 }
88 }
89
90 pub fn with_size(mut self, width: f32, height: f32) -> Self {
92 self.size = Vec2::new(width, height);
93 self
94 }
95
96 pub fn with_position(mut self, x: f32, y: f32) -> Self {
98 self.position = Pos2::new(x, y);
99 self
100 }
101
102 pub fn closable(mut self, closable: bool) -> Self {
104 self.closable = closable;
105 self
106 }
107
108 pub fn resizable(mut self, resizable: bool) -> Self {
110 self.resizable = resizable;
111 self
112 }
113
114 pub fn min_size(mut self, width: f32, height: f32) -> Self {
116 self.min_size = Vec2::new(width, height);
117 self
118 }
119
120 pub fn lock_level(mut self, level: LockLevel) -> Self {
122 self.lock_level = level;
123 self
124 }
125
126 pub fn with_icon(mut self, icon: &'static str) -> Self {
128 self.title_icon = Some(icon);
129 self
130 }
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
135pub enum LockLevel {
136 #[default]
138 None,
139 Light,
141 Full,
143}
144
145impl LockLevel {
146 pub fn cycle(self) -> Self {
148 match self {
149 LockLevel::None => LockLevel::Light,
150 LockLevel::Light => LockLevel::Full,
151 LockLevel::Full => LockLevel::None,
152 }
153 }
154
155 pub fn allows_move_resize(self) -> bool {
157 matches!(self, LockLevel::None)
158 }
159
160 pub fn allows_window_controls(self) -> bool {
162 !matches!(self, LockLevel::Full)
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
168pub enum ArrangeStrategy {
169 #[default]
171 ResolveOverlaps,
172 Grid { columns: Option<usize> },
174 Cascade,
176 Horizontal,
178 Vertical,
180}
181
182#[derive(Debug, Clone)]
184pub enum NodeLayoutEvent {
185 PaneMoved { id: String, position: Pos2 },
187 PaneResized { id: String, size: Vec2 },
189 PaneCollapsed { id: String, collapsed: bool },
191 PaneMaximized { id: String, maximized: bool },
193 PaneLockChanged { id: String, lock_level: LockLevel },
195 PaneClosed(String),
197 AutoArranged {
199 strategy: ArrangeStrategy,
200 moved_pane_ids: Vec<String>,
201 },
202 CanvasLockChanged(LockLevel),
204 ZoomToFit,
206 ZoomReset,
208}
209
210#[derive(Clone, Copy, Debug, PartialEq)]
212enum ResizeEdge {
213 Left,
214 Right,
215 Top,
216 Bottom,
217 TopLeft,
218 TopRight,
219 BottomLeft,
220 BottomRight,
221}
222
223#[derive(Clone)]
225struct LayoutState {
226 to_screen: TSTransform,
228 initialized: bool,
230 dragging: Option<String>,
232 resizing: Option<(String, ResizeEdge)>,
234 draw_order: Vec<String>,
236}
237
238impl Default for LayoutState {
239 fn default() -> Self {
240 Self {
241 to_screen: TSTransform::IDENTITY,
242 initialized: false,
243 dragging: None,
244 resizing: None,
245 draw_order: Vec::new(),
246 }
247 }
248}
249
250pub struct NodeLayout {
252 panes: Vec<LayoutPane>,
253 id_to_index: HashMap<String, usize>,
255}
256
257impl Default for NodeLayout {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263impl NodeLayout {
264 pub fn new() -> Self {
266 Self {
267 panes: Vec::new(),
268 id_to_index: HashMap::new(),
269 }
270 }
271
272 pub fn add_pane(&mut self, pane: LayoutPane, position: Pos2) -> &mut Self {
274 let mut pane = pane;
275 pane.position = position;
276 let id = pane.id.clone();
277 let index = self.panes.len();
278 self.panes.push(pane);
279 self.id_to_index.insert(id, index);
280 self
281 }
282
283 pub fn remove_pane(&mut self, id: &str) -> Option<LayoutPane> {
285 if let Some(index) = self.id_to_index.remove(id) {
286 let pane = self.panes.remove(index);
287 self.id_to_index.clear();
289 for (i, p) in self.panes.iter().enumerate() {
290 self.id_to_index.insert(p.id.clone(), i);
291 }
292 Some(pane)
293 } else {
294 None
295 }
296 }
297
298 pub fn get_pane(&self, id: &str) -> Option<&LayoutPane> {
300 self.id_to_index.get(id).map(|&i| &self.panes[i])
301 }
302
303 pub fn get_pane_mut(&mut self, id: &str) -> Option<&mut LayoutPane> {
305 if let Some(&i) = self.id_to_index.get(id) {
306 Some(&mut self.panes[i])
307 } else {
308 None
309 }
310 }
311
312 pub fn panes(&self) -> impl Iterator<Item = &LayoutPane> {
314 self.panes.iter()
315 }
316
317 pub fn panes_mut(&mut self) -> impl Iterator<Item = &mut LayoutPane> {
319 self.panes.iter_mut()
320 }
321
322 pub fn has_overlaps(&self, gap: f32) -> bool {
324 let rects: Vec<Rect> = self
325 .panes
326 .iter()
327 .filter(|p| !p.collapsed && !p.maximized)
328 .map(|p| Rect::from_min_size(p.position, p.size))
329 .collect();
330 super::layout_helpers::has_overlaps(&rects, gap)
331 }
332
333 pub fn resolve_overlaps(&mut self, gap: f32) -> Vec<String> {
337 let pane_data: Vec<(usize, Rect)> = self
339 .panes
340 .iter()
341 .enumerate()
342 .filter(|(_, p)| !p.collapsed && !p.maximized)
343 .map(|(i, p)| (i, Rect::from_min_size(p.position, p.size)))
344 .collect();
345
346 if pane_data.is_empty() {
347 return Vec::new();
348 }
349
350 let rects: Vec<Rect> = pane_data.iter().map(|(_, r)| *r).collect();
351 let result = super::layout_helpers::resolve_overlaps(&rects, gap, 100);
352
353 if !result.changed {
354 return Vec::new();
355 }
356
357 let mut moved_ids = Vec::new();
359 for (i, new_pos) in result.positions.iter().enumerate() {
360 let pane_idx = pane_data[i].0;
361 let pane = &mut self.panes[pane_idx];
362 if pane.position != *new_pos {
363 pane.position = *new_pos;
364 moved_ids.push(pane.id.clone());
365 }
366 }
367
368 moved_ids
369 }
370
371 pub fn resolve_overlaps_anchored(&mut self, gap: f32, anchor_strength: f32) -> Vec<String> {
376 let pane_data: Vec<(usize, Rect)> = self
377 .panes
378 .iter()
379 .enumerate()
380 .filter(|(_, p)| !p.collapsed && !p.maximized)
381 .map(|(i, p)| (i, Rect::from_min_size(p.position, p.size)))
382 .collect();
383
384 if pane_data.is_empty() {
385 return Vec::new();
386 }
387
388 let rects: Vec<Rect> = pane_data.iter().map(|(_, r)| *r).collect();
389 let result =
390 super::layout_helpers::resolve_overlaps_with_anchors(&rects, gap, anchor_strength, 100);
391
392 if !result.changed {
393 return Vec::new();
394 }
395
396 let mut moved_ids = Vec::new();
397 for (i, new_pos) in result.positions.iter().enumerate() {
398 let pane_idx = pane_data[i].0;
399 let pane = &mut self.panes[pane_idx];
400 if pane.position != *new_pos {
401 pane.position = *new_pos;
402 moved_ids.push(pane.id.clone());
403 }
404 }
405
406 moved_ids
407 }
408
409 pub fn count_overlaps(&self, gap: f32) -> usize {
411 let rects: Vec<Rect> = self
412 .panes
413 .iter()
414 .filter(|p| !p.collapsed && !p.maximized)
415 .map(|p| Rect::from_min_size(p.position, p.size))
416 .collect();
417 super::layout_helpers::count_overlaps(&rects, gap)
418 }
419
420 pub fn auto_arrange(
432 &mut self,
433 strategy: ArrangeStrategy,
434 gap: f32,
435 origin: Option<Pos2>,
436 z_order_ids: Option<&[String]>,
437 ) -> Vec<String> {
438 use super::layout_helpers;
439
440 let pane_data: Vec<(usize, Rect)> = self
446 .panes
447 .iter()
448 .enumerate()
449 .filter(|(_, p)| !p.collapsed && !p.maximized)
450 .map(|(i, p)| (i, Rect::from_min_size(p.position, p.size)))
451 .collect();
452
453 if pane_data.is_empty() {
454 return Vec::new();
455 }
456
457 let rects: Vec<Rect> = pane_data.iter().map(|(_, r)| *r).collect();
458
459 let origin = origin.unwrap_or_else(|| {
461 layout_helpers::bounding_box(&rects)
462 .map(|b| b.min)
463 .unwrap_or(Pos2::ZERO)
464 });
465
466 let result = match strategy {
468 ArrangeStrategy::ResolveOverlaps => {
469 layout_helpers::resolve_overlaps(&rects, gap, 100).into()
470 }
471 ArrangeStrategy::Grid { columns } => {
472 layout_helpers::arrange_grid_proportional(&rects, columns, origin, gap)
473 }
474 ArrangeStrategy::Cascade => {
475 let offset = Vec2::new(30.0, 30.0);
476
477 let cascade_order = z_order_ids.and_then(|ids| {
479 let order: Vec<usize> = ids
482 .iter()
483 .rev() .filter_map(|id| {
485 pane_data
486 .iter()
487 .position(|(idx, _)| self.panes[*idx].id == *id)
488 })
489 .collect();
490
491 if order.len() == pane_data.len() {
492 Some(order)
493 } else {
494 None }
496 });
497
498 match cascade_order {
499 Some(order) => {
500 layout_helpers::arrange_cascade(&rects, origin, offset, Some(&order))
501 }
502 None => layout_helpers::arrange_cascade(&rects, origin, offset, None),
503 }
504 }
505 ArrangeStrategy::Horizontal => {
506 layout_helpers::arrange_horizontal(&rects, origin, gap, false)
507 }
508 ArrangeStrategy::Vertical => {
509 layout_helpers::arrange_vertical(&rects, origin, gap, false)
510 }
511 };
512
513 if !result.changed {
514 return Vec::new();
515 }
516
517 let mut moved_ids = Vec::new();
519 for (i, new_pos) in result.positions.iter().enumerate() {
520 let pane_idx = pane_data[i].0;
521 let pane = &mut self.panes[pane_idx];
522 if pane.position != *new_pos {
523 pane.position = *new_pos;
524 moved_ids.push(pane.id.clone());
525 }
526 }
527
528 moved_ids
529 }
530
531 pub fn bounding_box(&self) -> Option<Rect> {
533 let rects: Vec<Rect> = self
534 .panes
535 .iter()
536 .filter(|p| !p.collapsed && !p.maximized)
537 .map(|p| Rect::from_min_size(p.position, p.size))
538 .collect();
539 super::layout_helpers::bounding_box(&rects)
540 }
541}
542
543pub struct NodeLayoutArea<'a, F> {
545 layout: &'a mut NodeLayout,
546 content_fn: F,
547 lock_level: LockLevel,
548 title_height: f32,
549 content_padding: Option<f32>,
550 grid_size: f32,
551 grid_alpha: u8,
552 min_scale: f32,
553 max_scale: f32,
554 show_menu_bar: bool,
556 menu_bar_height: f32,
558}
559
560impl<'a, F> NodeLayoutArea<'a, F>
561where
562 F: FnMut(&mut Ui, &LayoutPane),
563{
564 pub fn new(layout: &'a mut NodeLayout, content_fn: F) -> Self {
566 Self {
567 layout,
568 content_fn,
569 lock_level: LockLevel::None,
570 title_height: 24.0,
571 content_padding: None, grid_size: 50.0,
573 grid_alpha: 30,
574 min_scale: 0.25,
575 max_scale: 2.0,
576 show_menu_bar: false,
577 menu_bar_height: 28.0,
578 }
579 }
580
581 pub fn show_menu_bar(mut self, show: bool) -> Self {
583 self.show_menu_bar = show;
584 self
585 }
586
587 pub fn menu_bar_height(mut self, height: f32) -> Self {
589 self.menu_bar_height = height;
590 self
591 }
592
593 pub fn lock_level(mut self, level: LockLevel) -> Self {
595 self.lock_level = level;
596 self
597 }
598
599 pub fn locked(mut self, locked: bool) -> Self {
601 self.lock_level = if locked {
602 LockLevel::Full
603 } else {
604 LockLevel::None
605 };
606 self
607 }
608
609 pub fn title_height(mut self, height: f32) -> Self {
611 self.title_height = height;
612 self
613 }
614
615 pub fn content_padding(mut self, padding: f32) -> Self {
617 self.content_padding = Some(padding);
618 self
619 }
620
621 pub fn grid_size(mut self, size: f32) -> Self {
623 self.grid_size = size;
624 self
625 }
626
627 pub fn grid_alpha(mut self, alpha: u8) -> Self {
629 self.grid_alpha = alpha;
630 self
631 }
632
633 pub fn zoom_range(mut self, min: f32, max: f32) -> Self {
635 self.min_scale = min;
636 self.max_scale = max;
637 self
638 }
639
640 pub fn show(mut self, ui: &mut Ui) -> Vec<NodeLayoutEvent> {
642 let theme = Theme::current(ui.ctx());
643 let mut events = Vec::new();
644
645 let full_rect = ui.available_rect_before_wrap();
647
648 let (menu_rect, canvas_rect) = if self.show_menu_bar {
650 let menu_rect = Rect::from_min_size(
651 full_rect.min,
652 Vec2::new(full_rect.width(), self.menu_bar_height),
653 );
654 let canvas_rect = Rect::from_min_max(
655 Pos2::new(full_rect.min.x, full_rect.min.y + self.menu_bar_height),
656 full_rect.max,
657 );
658 (Some(menu_rect), canvas_rect)
659 } else {
660 (None, full_rect)
661 };
662
663 let rect = canvas_rect;
665
666 let state_id = ui.id().with("node_layout_state");
668 let mut state: LayoutState = ui.ctx().data(|d| d.get_temp(state_id)).unwrap_or_default();
669
670 self.sync_draw_order(&mut state);
672
673 if let Some(menu_rect) = menu_rect {
675 self.draw_menu_bar(ui, menu_rect, &theme, &mut events, &state.draw_order);
676 }
677
678 if !state.initialized {
680 state.to_screen = TSTransform::from_translation(rect.min.to_vec2());
681 state.initialized = true;
682 }
683
684 let mut to_screen = state.to_screen;
686 if self.lock_level.allows_move_resize() {
687 let canvas_response = ui.allocate_rect(rect, Sense::drag());
689 let mut scene_response = canvas_response;
690 Scene::new()
691 .zoom_range(self.min_scale..=self.max_scale)
692 .register_pan_and_zoom(ui, &mut scene_response, &mut to_screen);
693 }
694
695 for event in &events {
697 match event {
698 NodeLayoutEvent::ZoomToFit => {
699 if let Some(bounds) = self.layout.bounding_box() {
700 let margin = 20.0;
702 let bounds = bounds.expand(margin);
703
704 let scale_x = rect.width() / bounds.width();
706 let scale_y = rect.height() / bounds.height();
707 let scale = scale_x.min(scale_y).clamp(self.min_scale, self.max_scale);
708
709 let bounds_center = bounds.center();
711 let rect_center = rect.center();
712
713 to_screen = TSTransform::from_translation(
714 rect_center.to_vec2() - bounds_center.to_vec2() * scale,
715 ) * TSTransform::from_scaling(scale);
716 }
717 }
718 NodeLayoutEvent::ZoomReset => {
719 to_screen = TSTransform::from_translation(rect.min.to_vec2());
721 }
722 _ => {}
723 }
724 }
725
726 let from_screen = to_screen.inverse();
727
728 let viewport = from_screen * rect;
730
731 let bg_painter = ui.painter_at(rect);
733 bg_painter.rect_filled(rect, 0.0, theme.bg_secondary);
734
735 self.draw_grid_screen(
737 &bg_painter,
738 rect,
739 self.grid_size,
740 self.grid_alpha,
741 &to_screen,
742 &theme,
743 );
744
745 let painter = ui.painter_at(rect).clone();
747
748 let mut pane_to_top: Option<String> = None;
750 let mut pane_moved: Option<(String, Vec2)> = None;
751 let mut pane_resized: Option<(String, Vec2)> = None;
752 let mut pane_collapsed: Option<(String, bool)> = None;
753 let mut pane_maximized: Option<(String, bool)> = None;
754 let mut pane_lock_changed: Option<(String, LockLevel)> = None;
755 let mut pane_closed: Option<String> = None;
756
757 let button_size = self.title_height * 0.7;
759 let button_padding = self.title_height * 0.15;
760
761 let resize_edge_size = 6.0;
763
764 let draw_order: Vec<_> = state.draw_order.iter().rev().cloned().collect();
767 for pane_id in draw_order {
768 let Some(pane) = self.layout.get_pane(&pane_id) else {
769 continue;
770 };
771
772 let (pane_rect, is_maximized) = if pane.maximized {
774 (viewport, true)
776 } else {
777 (Rect::from_min_size(pane.position, pane.size), false)
778 };
779
780 let effective_pane_rect = if pane.collapsed && !is_maximized {
782 Rect::from_min_size(
783 pane_rect.min,
784 Vec2::new(pane_rect.width(), self.title_height),
785 )
786 } else {
787 pane_rect
788 };
789
790 let screen_pane_rect = to_screen * effective_pane_rect;
792
793 if !is_maximized && !rect.intersects(screen_pane_rect) {
795 continue;
796 }
797
798 let frame_stroke = if state.dragging.as_ref() == Some(&pane_id) {
800 Stroke::new(theme.border_width * 2.0, theme.primary)
801 } else {
802 Stroke::new(theme.border_width, theme.border)
803 };
804
805 painter.rect_filled(screen_pane_rect, theme.radius_sm, theme.bg_primary);
806 painter.rect_stroke(
807 screen_pane_rect,
808 theme.radius_sm,
809 frame_stroke,
810 egui::StrokeKind::Outside,
811 );
812
813 let scaled_title_height = self.title_height * to_screen.scaling;
815 let screen_title_rect = Rect::from_min_size(
816 screen_pane_rect.min,
817 Vec2::new(screen_pane_rect.width(), scaled_title_height),
818 );
819
820 let radius = theme.radius_sm as u8;
821 let title_rounding = if pane.collapsed {
822 CornerRadius::same(radius)
824 } else {
825 CornerRadius {
826 nw: radius,
827 ne: radius,
828 sw: 0,
829 se: 0,
830 }
831 };
832 painter.rect_filled(screen_title_rect, title_rounding, theme.bg_tertiary);
833
834 let scaled_button_size = button_size * to_screen.scaling;
836 let scaled_button_padding = button_padding * to_screen.scaling;
837 let mut button_x = screen_title_rect.max.x - scaled_button_padding - scaled_button_size;
838
839 let icon_font =
841 egui::FontId::new(scaled_button_size * 0.7, FontFamily::Name("icons".into()));
842
843 let pane_allows_window_ctrl = pane.lock_level.allows_window_controls();
845 let canvas_allows_window_ctrl = self.lock_level.allows_window_controls();
846 let window_ctrl_enabled = pane_allows_window_ctrl && canvas_allows_window_ctrl;
847
848 if pane.closable {
850 let close_rect = Rect::from_min_size(
851 Pos2::new(button_x, screen_title_rect.min.y + scaled_button_padding),
852 Vec2::splat(scaled_button_size),
853 );
854 let close_response = ui.interact(
855 close_rect,
856 ui.id().with(&pane_id).with("close"),
857 if window_ctrl_enabled {
858 Sense::click()
859 } else {
860 Sense::hover()
861 },
862 );
863 let close_color = if !window_ctrl_enabled {
864 theme.text_muted
865 } else if close_response.hovered() {
866 theme.state_danger
867 } else {
868 theme.text_secondary
869 };
870 painter.text(
871 close_rect.center(),
872 egui::Align2::CENTER_CENTER,
873 icons::X,
874 icon_font.clone(),
875 close_color,
876 );
877 if window_ctrl_enabled && close_response.clicked() {
878 pane_closed = Some(pane_id.clone());
879 }
880 button_x -= scaled_button_size + scaled_button_padding * 0.5;
881 }
882
883 let max_rect = Rect::from_min_size(
885 Pos2::new(button_x, screen_title_rect.min.y + scaled_button_padding),
886 Vec2::splat(scaled_button_size),
887 );
888 let max_response = ui.interact(
889 max_rect,
890 ui.id().with(&pane_id).with("maximize"),
891 if window_ctrl_enabled {
892 Sense::click()
893 } else {
894 Sense::hover()
895 },
896 );
897 let max_color = if !window_ctrl_enabled {
898 theme.text_muted
899 } else if max_response.hovered() {
900 theme.text_primary
901 } else {
902 theme.text_secondary
903 };
904 let max_icon = if pane.maximized {
905 icons::CORNERS_IN
906 } else {
907 icons::CORNERS_OUT
908 };
909 painter.text(
910 max_rect.center(),
911 egui::Align2::CENTER_CENTER,
912 max_icon,
913 icon_font.clone(),
914 max_color,
915 );
916 if window_ctrl_enabled && max_response.clicked() {
917 pane_maximized = Some((pane_id.clone(), !pane.maximized));
918 if !pane.maximized {
920 pane_to_top = Some(pane_id.clone());
921 }
922 }
923 button_x -= scaled_button_size + scaled_button_padding * 0.5;
924
925 let collapse_rect = Rect::from_min_size(
927 Pos2::new(button_x, screen_title_rect.min.y + scaled_button_padding),
928 Vec2::splat(scaled_button_size),
929 );
930 let collapse_response = ui.interact(
931 collapse_rect,
932 ui.id().with(&pane_id).with("collapse"),
933 if window_ctrl_enabled {
934 Sense::click()
935 } else {
936 Sense::hover()
937 },
938 );
939 let collapse_color = if !window_ctrl_enabled {
940 theme.text_muted
941 } else if collapse_response.hovered() {
942 theme.text_primary
943 } else {
944 theme.text_secondary
945 };
946 let collapse_icon = if pane.collapsed {
947 icons::CARET_DOWN
948 } else {
949 icons::CARET_UP
950 };
951 painter.text(
952 collapse_rect.center(),
953 egui::Align2::CENTER_CENTER,
954 collapse_icon,
955 icon_font.clone(),
956 collapse_color,
957 );
958 if window_ctrl_enabled && collapse_response.clicked() {
959 pane_collapsed = Some((pane_id.clone(), !pane.collapsed));
960 }
961 button_x -= scaled_button_size + scaled_button_padding * 0.5;
962
963 let lock_button_enabled = canvas_allows_window_ctrl;
966 let lock_rect = Rect::from_min_size(
967 Pos2::new(button_x, screen_title_rect.min.y + scaled_button_padding),
968 Vec2::splat(scaled_button_size),
969 );
970 let lock_response = ui.interact(
971 lock_rect,
972 ui.id().with(&pane_id).with("lock"),
973 if lock_button_enabled {
974 Sense::click()
975 } else {
976 Sense::hover()
977 },
978 );
979 let (lock_icon, lock_color) = if !lock_button_enabled {
980 (icons::LOCK, theme.text_muted)
981 } else {
982 match pane.lock_level {
983 LockLevel::None => (
984 icons::LOCK_OPEN,
985 if lock_response.hovered() {
986 theme.text_primary
987 } else {
988 theme.text_secondary
989 },
990 ),
991 LockLevel::Light => (
992 icons::LOCK,
993 if lock_response.hovered() {
994 theme.text_primary
995 } else {
996 theme.primary
997 },
998 ),
999 LockLevel::Full => (
1000 icons::LOCK,
1001 if lock_response.hovered() {
1002 theme.text_primary
1003 } else {
1004 theme.state_danger
1005 },
1006 ),
1007 }
1008 };
1009 painter.text(
1010 lock_rect.center(),
1011 egui::Align2::CENTER_CENTER,
1012 lock_icon,
1013 icon_font.clone(),
1014 lock_color,
1015 );
1016 if lock_button_enabled && lock_response.clicked() {
1017 pane_lock_changed = Some((pane_id.clone(), pane.lock_level.cycle()));
1018 }
1019
1020 let title_text_max_x = button_x - scaled_button_padding;
1022 let font_size = (theme.font_size_sm * to_screen.scaling).max(theme.font_size_xs);
1023 let text_padding = theme.spacing_sm * to_screen.scaling;
1024 let title_text_rect = Rect::from_min_max(
1025 screen_title_rect.min + Vec2::new(text_padding, 0.0),
1026 Pos2::new(title_text_max_x, screen_title_rect.max.y),
1027 );
1028 let clipped_painter = ui.painter().with_clip_rect(title_text_rect.intersect(rect));
1030
1031 let mut title_x = screen_title_rect.left_center().x + text_padding;
1033 if let Some(icon_char) = pane.title_icon {
1034 let icon_font = egui::FontId::new(font_size, FontFamily::Name("icons".into()));
1035 let icon_galley = clipped_painter.layout_no_wrap(
1036 icon_char.to_string(),
1037 icon_font.clone(),
1038 theme.text_secondary,
1039 );
1040 clipped_painter.galley(
1041 Pos2::new(
1042 title_x,
1043 screen_title_rect.center().y - icon_galley.size().y * 0.5,
1044 ),
1045 icon_galley.clone(),
1046 theme.text_secondary,
1047 );
1048 title_x += icon_galley.size().x + text_padding * 0.5;
1049 }
1050
1051 clipped_painter.text(
1053 Pos2::new(title_x, screen_title_rect.center().y),
1054 egui::Align2::LEFT_CENTER,
1055 &pane.title,
1056 egui::FontId::proportional(font_size),
1057 theme.text_primary,
1058 );
1059
1060 let title_drag_rect = Rect::from_min_max(
1062 screen_title_rect.min,
1063 Pos2::new(title_text_max_x, screen_title_rect.max.y),
1064 );
1065
1066 let pane_lock = pane.lock_level;
1068 let can_move_resize =
1069 self.lock_level.allows_move_resize() && pane_lock.allows_move_resize();
1070 let _can_window_control =
1071 self.lock_level.allows_window_controls() && pane_lock.allows_window_controls();
1072
1073 let title_response = ui.interact(
1074 title_drag_rect,
1075 ui.id().with(&pane_id).with("title_drag"),
1076 if !can_move_resize || is_maximized {
1077 Sense::hover()
1078 } else {
1079 Sense::click_and_drag()
1080 },
1081 );
1082
1083 if title_response.clicked() || title_response.drag_started() {
1084 pane_to_top = Some(pane_id.clone());
1085 }
1086
1087 if can_move_resize && !is_maximized && title_response.dragged() {
1088 if state.dragging.is_none() && state.resizing.is_none() {
1089 state.dragging = Some(pane_id.clone());
1090 }
1091
1092 if state.dragging.as_ref() == Some(&pane_id) {
1093 let screen_delta = title_response.drag_delta();
1095 let graph_delta = screen_delta / to_screen.scaling;
1096 pane_moved = Some((pane_id.clone(), graph_delta));
1097 }
1098 }
1099
1100 if title_response.drag_stopped() && state.dragging.as_ref() == Some(&pane_id) {
1101 state.dragging = None;
1102 }
1103
1104 if !pane.collapsed && !is_maximized && pane.resizable && can_move_resize {
1107 let detect_edge = |pos: Pos2| -> Option<ResizeEdge> {
1109 let expanded = screen_pane_rect.expand(resize_edge_size);
1111 if !expanded.contains(pos) {
1112 return None;
1113 }
1114
1115 let in_left = pos.x < screen_pane_rect.min.x + resize_edge_size;
1116 let in_right = pos.x > screen_pane_rect.max.x - resize_edge_size;
1117 let in_top = pos.y < screen_pane_rect.min.y + resize_edge_size;
1118 let in_bottom = pos.y > screen_pane_rect.max.y - resize_edge_size;
1119
1120 match (in_left, in_right, in_top, in_bottom) {
1121 (true, _, true, _) => Some(ResizeEdge::TopLeft),
1122 (true, _, _, true) => Some(ResizeEdge::BottomLeft),
1123 (_, true, true, _) => Some(ResizeEdge::TopRight),
1124 (_, true, _, true) => Some(ResizeEdge::BottomRight),
1125 (true, _, _, _) => Some(ResizeEdge::Left),
1126 (_, true, _, _) => Some(ResizeEdge::Right),
1127 (_, _, true, _) => Some(ResizeEdge::Top),
1128 (_, _, _, true) => Some(ResizeEdge::Bottom),
1129 _ => None,
1130 }
1131 };
1132
1133 let pointer = ui.input(|i| i.pointer.clone());
1135
1136 if let Some(hover_pos) = pointer.hover_pos() {
1138 if let Some(edge) = detect_edge(hover_pos) {
1139 let cursor = match edge {
1140 ResizeEdge::Left | ResizeEdge::Right => {
1141 egui::CursorIcon::ResizeHorizontal
1142 }
1143 ResizeEdge::Top | ResizeEdge::Bottom => {
1144 egui::CursorIcon::ResizeVertical
1145 }
1146 ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
1147 egui::CursorIcon::ResizeNwSe
1148 }
1149 ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
1150 egui::CursorIcon::ResizeNeSw
1151 }
1152 };
1153 ui.ctx().set_cursor_icon(cursor);
1154 }
1155 }
1156
1157 if pointer.any_pressed() && state.resizing.is_none() && state.dragging.is_none() {
1159 if let Some(pos) = pointer.press_origin() {
1160 if let Some(edge) = detect_edge(pos) {
1161 state.resizing = Some((pane_id.clone(), edge));
1162 pane_to_top = Some(pane_id.clone());
1163 }
1164 }
1165 }
1166
1167 if let Some((ref resize_id, edge)) = state.resizing.clone() {
1169 if resize_id == &pane_id {
1170 if pointer.is_decidedly_dragging() {
1171 let delta = pointer.delta() / to_screen.scaling;
1172 let min_size = pane.min_size;
1173
1174 let mut new_pos = pane.position;
1176 let mut new_size = pane.size;
1177
1178 match edge {
1179 ResizeEdge::Right => {
1180 new_size.x = (new_size.x + delta.x).max(min_size.x);
1181 }
1182 ResizeEdge::Bottom => {
1183 new_size.y = (new_size.y + delta.y).max(min_size.y);
1184 }
1185 ResizeEdge::Left => {
1186 let new_width = (new_size.x - delta.x).max(min_size.x);
1187 new_pos.x += new_size.x - new_width;
1188 new_size.x = new_width;
1189 }
1190 ResizeEdge::Top => {
1191 let new_height = (new_size.y - delta.y).max(min_size.y);
1192 new_pos.y += new_size.y - new_height;
1193 new_size.y = new_height;
1194 }
1195 ResizeEdge::TopLeft => {
1196 let new_width = (new_size.x - delta.x).max(min_size.x);
1197 let new_height = (new_size.y - delta.y).max(min_size.y);
1198 new_pos.x += new_size.x - new_width;
1199 new_pos.y += new_size.y - new_height;
1200 new_size = Vec2::new(new_width, new_height);
1201 }
1202 ResizeEdge::TopRight => {
1203 let new_height = (new_size.y - delta.y).max(min_size.y);
1204 new_pos.y += new_size.y - new_height;
1205 new_size.x = (new_size.x + delta.x).max(min_size.x);
1206 new_size.y = new_height;
1207 }
1208 ResizeEdge::BottomLeft => {
1209 let new_width = (new_size.x - delta.x).max(min_size.x);
1210 new_pos.x += new_size.x - new_width;
1211 new_size.x = new_width;
1212 new_size.y = (new_size.y + delta.y).max(min_size.y);
1213 }
1214 ResizeEdge::BottomRight => {
1215 new_size.x = (new_size.x + delta.x).max(min_size.x);
1216 new_size.y = (new_size.y + delta.y).max(min_size.y);
1217 }
1218 }
1219
1220 if new_pos != pane.position {
1221 pane_moved = Some((pane_id.clone(), new_pos - pane.position));
1222 }
1223 if new_size != pane.size {
1224 pane_resized = Some((pane_id.clone(), new_size));
1225 }
1226 }
1227
1228 if pointer.any_released() {
1230 state.resizing = None;
1231 }
1232 }
1233 }
1234 }
1235
1236 if !pane.collapsed {
1238 let base_padding = self.content_padding.unwrap_or(theme.spacing_sm);
1239 let content_padding = base_padding * to_screen.scaling;
1240 let screen_content_rect = Rect::from_min_max(
1241 screen_pane_rect.min
1242 + Vec2::new(content_padding, scaled_title_height + content_padding),
1243 screen_pane_rect.max - Vec2::new(content_padding, content_padding),
1244 );
1245
1246 let clipped_content_rect = screen_content_rect.intersect(rect);
1248
1249 let min_size = theme.spacing_sm * to_screen.scaling;
1251 if clipped_content_rect.height() > min_size
1252 && clipped_content_rect.width() > min_size
1253 {
1254 let mut child_ui = ui.new_child(
1256 egui::UiBuilder::new()
1257 .max_rect(screen_content_rect)
1258 .layout(egui::Layout::top_down(egui::Align::LEFT)),
1259 );
1260 child_ui.set_clip_rect(clipped_content_rect);
1261 child_ui.spacing_mut().item_spacing =
1262 egui::vec2(theme.spacing_xs, theme.spacing_xs);
1263
1264 let pane_ref = self.layout.get_pane(&pane_id).unwrap();
1265 (self.content_fn)(&mut child_ui, pane_ref);
1266 }
1267 }
1268 }
1269
1270 if let Some((id, delta)) = pane_moved {
1272 if let Some(pane) = self.layout.get_pane_mut(&id) {
1273 pane.position += delta;
1274 events.push(NodeLayoutEvent::PaneMoved {
1275 id: id.clone(),
1276 position: pane.position,
1277 });
1278 }
1279 }
1280
1281 if let Some((id, new_size)) = pane_resized {
1283 if let Some(pane) = self.layout.get_pane_mut(&id) {
1284 pane.size = new_size;
1285 events.push(NodeLayoutEvent::PaneResized {
1286 id: id.clone(),
1287 size: new_size,
1288 });
1289 }
1290 }
1291
1292 if let Some((id, collapsed)) = pane_collapsed {
1294 if let Some(pane) = self.layout.get_pane_mut(&id) {
1295 pane.collapsed = collapsed;
1296 events.push(NodeLayoutEvent::PaneCollapsed {
1297 id: id.clone(),
1298 collapsed,
1299 });
1300 }
1301 }
1302
1303 if let Some((id, maximized)) = pane_maximized {
1305 if let Some(pane) = self.layout.get_pane_mut(&id) {
1306 if maximized {
1307 pane.pre_maximize_size = Some(pane.size);
1309 pane.pre_maximize_position = Some(pane.position);
1310 } else {
1311 if let Some(size) = pane.pre_maximize_size.take() {
1313 pane.size = size;
1314 }
1315 if let Some(pos) = pane.pre_maximize_position.take() {
1316 pane.position = pos;
1317 }
1318 }
1319 pane.maximized = maximized;
1320 events.push(NodeLayoutEvent::PaneMaximized {
1321 id: id.clone(),
1322 maximized,
1323 });
1324 }
1325 }
1326
1327 if let Some((id, new_level)) = pane_lock_changed {
1329 if let Some(pane) = self.layout.get_pane_mut(&id) {
1330 pane.lock_level = new_level;
1331 events.push(NodeLayoutEvent::PaneLockChanged {
1332 id: id.clone(),
1333 lock_level: new_level,
1334 });
1335 }
1336 }
1337
1338 if let Some(id) = pane_closed {
1340 self.layout.remove_pane(&id);
1341 events.push(NodeLayoutEvent::PaneClosed(id));
1342 }
1343
1344 if let Some(id) = pane_to_top {
1346 if let Some(pos) = state.draw_order.iter().position(|x| x == &id) {
1347 state.draw_order.remove(pos);
1348 state.draw_order.insert(0, id);
1349 }
1350 }
1351
1352 state.to_screen = to_screen;
1354 ui.ctx().data_mut(|d| d.insert_temp(state_id, state));
1355
1356 events
1357 }
1358
1359 fn sync_draw_order(&self, state: &mut LayoutState) {
1360 state
1362 .draw_order
1363 .retain(|id| self.layout.id_to_index.contains_key(id));
1364
1365 for pane in &self.layout.panes {
1367 if !state.draw_order.contains(&pane.id) {
1368 state.draw_order.push(pane.id.clone());
1369 }
1370 }
1371 }
1372
1373 fn draw_menu_bar(
1375 &mut self,
1376 ui: &mut Ui,
1377 rect: Rect,
1378 theme: &Theme,
1379 events: &mut Vec<NodeLayoutEvent>,
1380 draw_order: &[String],
1381 ) {
1382 use crate::atoms::icons;
1383
1384 let painter = ui.painter_at(rect);
1386 painter.rect_filled(rect, 0.0, theme.bg_primary);
1387 painter.hline(
1388 rect.x_range(),
1389 rect.max.y,
1390 egui::Stroke::new(1.0, theme.border),
1391 );
1392
1393 let inner_rect = rect.shrink2(Vec2::new(theme.spacing_sm, 2.0));
1395
1396 ui.allocate_ui_at_rect(inner_rect, |child_ui| {
1398 child_ui.horizontal_centered(|child_ui| {
1399 child_ui.style_mut().spacing.item_spacing = Vec2::new(4.0, 0.0);
1400
1401 let icon_text = |icon: &str| -> egui::RichText {
1403 egui::RichText::new(icon).family(FontFamily::Name("icons".into()))
1404 };
1405
1406 let (lock_icon, lock_tooltip) = match self.lock_level {
1408 LockLevel::None => (icons::LOCK_OPEN, "Unlocked - Click to lock position"),
1409 LockLevel::Light => (icons::LOCK_KEY, "Position locked - Click to fully lock"),
1410 LockLevel::Full => (icons::LOCK, "Fully locked - Click to unlock"),
1411 };
1412
1413 if child_ui
1414 .add(egui::Button::new(icon_text(lock_icon)).min_size(Vec2::new(24.0, 20.0)))
1415 .on_hover_text(lock_tooltip)
1416 .clicked()
1417 {
1418 let new_level = match self.lock_level {
1419 LockLevel::None => LockLevel::Light,
1420 LockLevel::Light => LockLevel::Full,
1421 LockLevel::Full => LockLevel::None,
1422 };
1423 self.lock_level = new_level;
1424 events.push(NodeLayoutEvent::CanvasLockChanged(new_level));
1425 }
1426
1427 child_ui.separator();
1428
1429 let gap = super::layout_helpers::DEFAULT_GAP;
1431
1432 child_ui.menu_button(icon_text(icons::SLIDERS_HORIZONTAL), |ui| {
1433 use crate::atoms::ListItem;
1434
1435 if ListItem::new("Grid")
1436 .icon(icons::GRID_FOUR)
1437 .compact()
1438 .show(ui)
1439 .clicked()
1440 {
1441 let moved = self.layout.auto_arrange(
1442 ArrangeStrategy::Grid { columns: None },
1443 gap,
1444 None,
1445 None,
1446 );
1447 if !moved.is_empty() {
1448 events.push(NodeLayoutEvent::AutoArranged {
1449 strategy: ArrangeStrategy::Grid { columns: None },
1450 moved_pane_ids: moved,
1451 });
1452 }
1453 ui.close();
1454 }
1455 if ListItem::new("Horizontal")
1456 .icon(icons::ARROWS_OUT_LINE_HORIZONTAL)
1457 .compact()
1458 .show(ui)
1459 .clicked()
1460 {
1461 let moved =
1462 self.layout
1463 .auto_arrange(ArrangeStrategy::Horizontal, gap, None, None);
1464 if !moved.is_empty() {
1465 events.push(NodeLayoutEvent::AutoArranged {
1466 strategy: ArrangeStrategy::Horizontal,
1467 moved_pane_ids: moved,
1468 });
1469 }
1470 ui.close();
1471 }
1472 if ListItem::new("Vertical")
1473 .icon(icons::ARROWS_OUT_LINE_VERTICAL)
1474 .compact()
1475 .show(ui)
1476 .clicked()
1477 {
1478 let moved =
1479 self.layout
1480 .auto_arrange(ArrangeStrategy::Vertical, gap, None, None);
1481 if !moved.is_empty() {
1482 events.push(NodeLayoutEvent::AutoArranged {
1483 strategy: ArrangeStrategy::Vertical,
1484 moved_pane_ids: moved,
1485 });
1486 }
1487 ui.close();
1488 }
1489 if ListItem::new("Cascade")
1490 .icon(icons::SQUARES_FOUR)
1491 .compact()
1492 .show(ui)
1493 .clicked()
1494 {
1495 let moved = self.layout.auto_arrange(
1497 ArrangeStrategy::Cascade,
1498 gap,
1499 None,
1500 Some(draw_order),
1501 );
1502 if !moved.is_empty() {
1503 events.push(NodeLayoutEvent::AutoArranged {
1504 strategy: ArrangeStrategy::Cascade,
1505 moved_pane_ids: moved,
1506 });
1507 }
1508 ui.close();
1509 }
1510 ui.separator();
1511 if ListItem::new("Resolve Overlaps")
1512 .icon(icons::BROOM)
1513 .compact()
1514 .show(ui)
1515 .clicked()
1516 {
1517 let moved = self.layout.auto_arrange(
1518 ArrangeStrategy::ResolveOverlaps,
1519 gap,
1520 None,
1521 None,
1522 );
1523 if !moved.is_empty() {
1524 events.push(NodeLayoutEvent::AutoArranged {
1525 strategy: ArrangeStrategy::ResolveOverlaps,
1526 moved_pane_ids: moved,
1527 });
1528 }
1529 ui.close();
1530 }
1531 });
1532
1533 child_ui.separator();
1534
1535 if child_ui
1537 .add(
1538 egui::Button::new(icon_text(icons::FRAME_CORNERS))
1539 .min_size(Vec2::new(28.0, 20.0)),
1540 )
1541 .on_hover_text("Zoom to fit all panes")
1542 .clicked()
1543 {
1544 events.push(NodeLayoutEvent::ZoomToFit);
1545 }
1546
1547 if child_ui
1548 .add(egui::Button::new("100%").min_size(Vec2::new(40.0, 20.0)))
1549 .on_hover_text("Reset zoom to 100%")
1550 .clicked()
1551 {
1552 events.push(NodeLayoutEvent::ZoomReset);
1553 }
1554
1555 child_ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
1557 ui.add_space(theme.spacing_sm);
1558 let visible_count = self.layout.panes.iter().filter(|p| !p.collapsed).count();
1559 let total_count = self.layout.panes.len();
1560 ui.label(
1561 egui::RichText::new(format!("{}/{} panes", visible_count, total_count))
1562 .color(theme.text_secondary)
1563 .small(),
1564 );
1565 });
1566 }); }); }
1569
1570 fn draw_grid_screen(
1571 &self,
1572 painter: &egui::Painter,
1573 rect: Rect,
1574 grid_size: f32,
1575 grid_alpha: u8,
1576 to_screen: &TSTransform,
1577 theme: &Theme,
1578 ) {
1579 let grid_color = Color32::from_rgba_unmultiplied(
1580 theme.border.r(),
1581 theme.border.g(),
1582 theme.border.b(),
1583 grid_alpha,
1584 );
1585
1586 let from_screen = to_screen.inverse();
1587 let viewport = from_screen * rect;
1588
1589 let screen_grid_size = grid_size * to_screen.scaling;
1591
1592 if screen_grid_size < 10.0 || screen_grid_size > 500.0 {
1594 return;
1595 }
1596
1597 let start_x = (viewport.min.x / grid_size).floor() * grid_size;
1599 let mut x = start_x;
1600 while x <= viewport.max.x {
1601 let screen_x = to_screen.translation.x + x * to_screen.scaling;
1602 if screen_x >= rect.min.x && screen_x <= rect.max.x {
1603 painter.line_segment(
1604 [
1605 Pos2::new(screen_x, rect.min.y),
1606 Pos2::new(screen_x, rect.max.y),
1607 ],
1608 Stroke::new(theme.stroke_width, grid_color),
1609 );
1610 }
1611 x += grid_size;
1612 }
1613
1614 let start_y = (viewport.min.y / grid_size).floor() * grid_size;
1616 let mut y = start_y;
1617 while y <= viewport.max.y {
1618 let screen_y = to_screen.translation.y + y * to_screen.scaling;
1619 if screen_y >= rect.min.y && screen_y <= rect.max.y {
1620 painter.line_segment(
1621 [
1622 Pos2::new(rect.min.x, screen_y),
1623 Pos2::new(rect.max.x, screen_y),
1624 ],
1625 Stroke::new(theme.stroke_width, grid_color),
1626 );
1627 }
1628 y += grid_size;
1629 }
1630 }
1631}