1use egui::{NumExt as _, Rect, Ui};
2
3use crate::behavior::EditAction;
4use crate::{ContainerInsertion, ContainerKind, UiResponse};
5
6use super::{
7 Behavior, Container, DropContext, InsertionPoint, SimplificationOptions, SimplifyAction, Tile,
8 TileId, Tiles,
9};
10
11#[derive(Clone, PartialEq)]
30#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
31pub struct Tree<Pane> {
32 pub(crate) id: egui::Id,
34
35 pub root: Option<TileId>,
37
38 pub tiles: Tiles<Pane>,
40
41 #[cfg_attr(
43 feature = "serde",
44 serde(serialize_with = "serialize_f32_infinity_as_null"),
45 serde(deserialize_with = "deserialize_f32_null_as_infinity")
46 )]
47 height: f32,
48
49 #[cfg_attr(
51 feature = "serde",
52 serde(serialize_with = "serialize_f32_infinity_as_null"),
53 serde(deserialize_with = "deserialize_f32_null_as_infinity")
54 )]
55 width: f32,
56}
57
58#[cfg(feature = "serde")]
60fn serialize_f32_infinity_as_null<S: serde::Serializer>(
61 t: &f32,
62 serializer: S,
63) -> Result<S::Ok, S::Error> {
64 if t.is_infinite() {
65 serializer.serialize_none()
66 } else {
67 serializer.serialize_some(t)
68 }
69}
70
71#[cfg(feature = "serde")]
72fn deserialize_f32_null_as_infinity<'de, D: serde::Deserializer<'de>>(
73 des: D,
74) -> Result<f32, D::Error> {
75 use serde::Deserialize as _;
76 Ok(Option::<f32>::deserialize(des)?.unwrap_or(f32::INFINITY))
77}
78
79impl<Pane: std::fmt::Debug> std::fmt::Debug for Tree<Pane> {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 fn format_tile<Pane: std::fmt::Debug>(
83 f: &mut std::fmt::Formatter<'_>,
84 tiles: &Tiles<Pane>,
85 indent: usize,
86 tile_id: TileId,
87 ) -> std::fmt::Result {
88 write!(f, "{} {tile_id:?}: ", " ".repeat(indent))?;
89 if let Some(tile) = tiles.get(tile_id) {
90 match tile {
91 Tile::Pane(pane) => writeln!(f, "Pane {pane:?}"),
92 Tile::Container(container) => {
93 writeln!(
94 f,
95 "{}",
96 match container {
97 Container::Tabs(_) => "Tabs",
98 Container::Linear(_) => "Linear",
99 Container::Grid(_) => "Grid",
100 }
101 )?;
102 for &child in container.children() {
103 format_tile(f, tiles, indent + 1, child)?;
104 }
105 Ok(())
106 }
107 }
108 } else {
109 writeln!(f, "DANGLING")
110 }
111 }
112
113 let Self {
114 id,
115 root,
116 tiles,
117 width,
118 height,
119 } = self;
120
121 if let Some(root) = root {
122 writeln!(f, "Tree {{")?;
123 writeln!(f, " id: {id:?}")?;
124 writeln!(f, " width: {width:?}")?;
125 writeln!(f, " height: {height:?}")?;
126 format_tile(f, tiles, 1, *root)?;
127 write!(f, "}}")
128 } else {
129 writeln!(f, "Tree {{ }}")
130 }
131 }
132}
133
134impl<Pane> Tree<Pane> {
137 pub fn empty(id: impl Into<egui::Id>) -> Self {
142 Self {
143 id: id.into(),
144 root: None,
145 tiles: Default::default(),
146 width: f32::INFINITY,
147 height: f32::INFINITY,
148 }
149 }
150
151 pub fn new(id: impl Into<egui::Id>, root: TileId, tiles: Tiles<Pane>) -> Self {
157 Self {
158 id: id.into(),
159 root: Some(root),
160 tiles,
161 width: f32::INFINITY,
162 height: f32::INFINITY,
163 }
164 }
165
166 pub fn new_tabs(id: impl Into<egui::Id>, panes: Vec<Pane>) -> Self {
171 Self::new_container(id, ContainerKind::Tabs, panes)
172 }
173
174 pub fn new_horizontal(id: impl Into<egui::Id>, panes: Vec<Pane>) -> Self {
179 Self::new_container(id, ContainerKind::Horizontal, panes)
180 }
181
182 pub fn new_vertical(id: impl Into<egui::Id>, panes: Vec<Pane>) -> Self {
187 Self::new_container(id, ContainerKind::Vertical, panes)
188 }
189
190 pub fn new_grid(id: impl Into<egui::Id>, panes: Vec<Pane>) -> Self {
195 Self::new_container(id, ContainerKind::Grid, panes)
196 }
197
198 pub fn new_container(id: impl Into<egui::Id>, kind: ContainerKind, panes: Vec<Pane>) -> Self {
203 let mut tiles = Tiles::default();
204 let tile_ids = panes
205 .into_iter()
206 .map(|pane| tiles.insert_pane(pane))
207 .collect();
208 let root = tiles.insert_new(Tile::Container(Container::new(kind, tile_ids)));
209 Self::new(id, root, tiles)
210 }
211
212 pub fn remove_recursively(&mut self, id: TileId) -> Vec<Tile<Pane>> {
218 self.remove_tile_id_from_parent(id);
220
221 let mut removed_tiles = vec![];
222 self.remove_recursively_impl(id, &mut removed_tiles);
223 removed_tiles
224 }
225
226 fn remove_recursively_impl(&mut self, id: TileId, removed_tiles: &mut Vec<Tile<Pane>>) {
227 if let Some(tile) = self.tiles.remove(id) {
231 if let Tile::Container(container) = &tile {
232 for &child_id in container.children() {
233 self.remove_recursively_impl(child_id, removed_tiles);
234 }
235 }
236 removed_tiles.push(tile);
237 }
238 }
239
240 #[inline]
242 pub fn id(&self) -> egui::Id {
243 self.id
244 }
245
246 #[inline]
248 pub fn is_empty(&self) -> bool {
249 self.root.is_none()
250 }
251
252 #[inline]
253 pub fn root(&self) -> Option<TileId> {
254 self.root
255 }
256
257 #[inline]
258 pub fn is_root(&self, tile: TileId) -> bool {
259 self.root == Some(tile)
260 }
261
262 pub fn is_visible(&self, tile_id: TileId) -> bool {
266 self.tiles.is_visible(tile_id)
267 }
268
269 pub fn set_visible(&mut self, tile_id: TileId, visible: bool) {
273 self.tiles.set_visible(tile_id, visible);
274 }
275
276 pub fn active_tiles(&self) -> Vec<TileId> {
282 let mut tiles = vec![];
283 if let Some(root) = self.root {
284 if self.is_visible(root) {
285 self.tiles.collect_active_tiles(root, &mut tiles);
286 }
287 }
288 tiles
289 }
290
291 pub fn inactive_tiles(&self) -> Vec<TileId> {
297 let active_tiles = self.active_tiles();
298 self.tiles
299 .tile_ids()
300 .filter(|id| !active_tiles.contains(id))
301 .collect()
302 }
303
304 pub fn ui(&mut self, behavior: &mut dyn Behavior<Pane>, ui: &mut Ui) {
308 self.simplify(&behavior.simplification_options());
309
310 self.gc(behavior);
311
312 self.tiles.rects.clear();
313
314 let mut drop_context = DropContext {
316 enabled: true,
317 dragged_tile_id: self.dragged_id(ui.ctx()),
318 mouse_pos: ui.input(|i| i.pointer.interact_pos()),
319 best_dist_sq: f32::INFINITY,
320 best_insertion: None,
321 preview_rect: None,
322 };
323
324 let mut rect = ui.available_rect_before_wrap();
325 if self.height.is_finite() {
326 rect.set_height(self.height);
327 }
328 if self.width.is_finite() {
329 rect.set_width(self.width);
330 }
331 if let Some(root) = self.root {
332 self.tiles.layout_tile(ui.style(), behavior, rect, root);
333
334 self.tile_ui(behavior, &mut drop_context, ui, root);
335 }
336
337 self.preview_dragged_tile(behavior, &drop_context, ui);
338 ui.advance_cursor_after_rect(rect);
339 }
340
341 pub fn set_height(&mut self, height: f32) {
346 if height.is_sign_positive() && height.is_finite() {
347 self.height = height;
348 } else {
349 self.height = f32::INFINITY;
350 }
351 }
352
353 pub fn set_width(&mut self, width: f32) {
358 if width.is_sign_positive() && width.is_finite() {
359 self.width = width;
360 } else {
361 self.width = f32::INFINITY;
362 }
363 }
364
365 pub(super) fn tile_ui(
366 &mut self,
367 behavior: &mut dyn Behavior<Pane>,
368 drop_context: &mut DropContext,
369 ui: &Ui,
370 tile_id: TileId,
371 ) {
372 if !self.is_visible(tile_id) {
373 return;
374 }
375 let Some(rect) = self.tiles.rect(tile_id) else {
378 log::debug!("Failed to find rect for tile {tile_id:?} during ui");
379 return;
380 };
381 let Some(mut tile) = self.tiles.remove(tile_id) else {
382 log::debug!("Failed to find tile {tile_id:?} during ui");
383 return;
384 };
385
386 let drop_context_was_enabled = drop_context.enabled;
387 if Some(tile_id) == drop_context.dragged_tile_id {
388 drop_context.enabled = false;
390 }
391 drop_context.on_tile(behavior, ui.style(), tile_id, rect, &tile);
392
393 let enabled = ui.is_enabled();
395 let mut ui = egui::Ui::new(
396 ui.ctx().clone(),
397 ui.id().with(tile_id),
398 egui::UiBuilder::new()
399 .layer_id(ui.layer_id())
400 .max_rect(rect),
401 );
402
403 ui.add_enabled_ui(enabled, |ui| {
404 match &mut tile {
405 Tile::Pane(pane) => {
406 if behavior.pane_ui(ui, tile_id, pane) == UiResponse::DragStarted {
407 ui.ctx().set_dragged_id(tile_id.egui_id(self.id));
408 }
409 }
410 Tile::Container(container) => {
411 container.ui(self, behavior, drop_context, ui, rect, tile_id);
412 }
413 };
414
415 behavior.paint_on_top_of_tile(ui.painter(), ui.style(), tile_id, rect);
416
417 self.tiles.insert(tile_id, tile);
418 drop_context.enabled = drop_context_was_enabled;
419 });
420 }
421
422 pub fn make_active(
428 &mut self,
429 mut should_activate: impl FnMut(TileId, &Tile<Pane>) -> bool,
430 ) -> bool {
431 if let Some(root) = self.root {
432 self.tiles.make_active(root, &mut should_activate)
433 } else {
434 false
435 }
436 }
437
438 fn preview_dragged_tile(
439 &mut self,
440 behavior: &mut dyn Behavior<Pane>,
441 drop_context: &DropContext,
442 ui: &mut Ui,
443 ) {
444 let (Some(mouse_pos), Some(dragged_tile_id)) =
445 (drop_context.mouse_pos, drop_context.dragged_tile_id)
446 else {
447 return;
448 };
449
450 ui.output_mut(|o| o.cursor_icon = egui::CursorIcon::Grabbing);
451
452 egui::Area::new(ui.id().with((dragged_tile_id, "preview")))
454 .pivot(egui::Align2::CENTER_CENTER)
455 .current_pos(mouse_pos)
456 .interactable(false)
457 .show(ui.ctx(), |ui| {
458 behavior.drag_ui(&self.tiles, ui, dragged_tile_id);
459 });
460
461 if let Some(preview_rect) = drop_context.preview_rect {
462 let preview_rect = smooth_preview_rect(ui.ctx(), dragged_tile_id, preview_rect);
463
464 let parent_rect = drop_context
465 .best_insertion
466 .and_then(|insertion_point| self.tiles.rect(insertion_point.parent_id));
467
468 behavior.paint_drag_preview(ui.visuals(), ui.painter(), parent_rect, preview_rect);
469
470 if behavior.preview_dragged_panes() {
471 if preview_rect.width() > 32.0 && preview_rect.height() > 32.0 {
473 if let Some(Tile::Pane(pane)) = self.tiles.get_mut(dragged_tile_id) {
474 let _ignored: UiResponse = behavior.pane_ui(
477 &mut ui.new_child(egui::UiBuilder::new().max_rect(preview_rect)),
478 dragged_tile_id,
479 pane,
480 );
481 }
482 }
483 }
484 }
485
486 if ui.input(|i| i.pointer.any_released()) {
487 if let Some(insertion_point) = drop_context.best_insertion {
488 behavior.on_edit(EditAction::TileDropped);
489 self.move_tile(dragged_tile_id, insertion_point, false);
490 }
491 clear_smooth_preview_rect(ui.ctx(), dragged_tile_id);
492 }
493 }
494
495 pub fn simplify(&mut self, options: &SimplificationOptions) {
499 if let Some(root) = self.root {
500 match self.tiles.simplify(options, root, None) {
501 SimplifyAction::Keep => {}
502 SimplifyAction::Remove => {
503 self.root = None;
504 }
505 SimplifyAction::Replace(new_root) => {
506 self.root = Some(new_root);
507 }
508 }
509
510 if options.all_panes_must_have_tabs {
511 if let Some(tile_id) = self.root {
512 self.tiles.make_all_panes_children_of_tabs(false, tile_id);
513 }
514 }
515 }
516 }
517
518 pub fn simplify_children_of_tile(&mut self, tile_id: TileId, options: &SimplificationOptions) {
520 if let Some(Tile::Container(mut container)) = self.tiles.remove(tile_id) {
521 let kind = container.kind();
522 container.simplify_children(|child| self.tiles.simplify(options, child, Some(kind)));
523 self.tiles.insert(tile_id, Tile::Container(container));
524 }
525 }
526
527 pub fn gc(&mut self, behavior: &mut dyn Behavior<Pane>) {
531 self.tiles.gc_root(behavior, self.root);
532 }
533
534 pub fn move_tile_to_container(
549 &mut self,
550 moved_tile_id: TileId,
551 destination_container: TileId,
552 mut insertion_index: usize,
553 reflow_grid: bool,
554 ) {
555 if let Some(Tile::Container(target_container)) = self.tiles.get(destination_container) {
557 let num_children = target_container.num_children();
558 if insertion_index > num_children {
559 insertion_index = num_children;
560 }
561
562 let container_insertion = match target_container.kind() {
563 ContainerKind::Tabs => ContainerInsertion::Tabs(insertion_index),
564 ContainerKind::Horizontal => ContainerInsertion::Horizontal(insertion_index),
565 ContainerKind::Vertical => ContainerInsertion::Vertical(insertion_index),
566 ContainerKind::Grid => ContainerInsertion::Grid(insertion_index),
567 };
568
569 self.move_tile(
570 moved_tile_id,
571 InsertionPoint {
572 parent_id: destination_container,
573 insertion: container_insertion,
574 },
575 reflow_grid,
576 );
577 } else {
578 log::warn!(
579 "Failed to find destination container {destination_container:?} during `move_tile_to_container()`"
580 );
581 }
582 }
583
584 pub(super) fn move_tile(
588 &mut self,
589 moved_tile_id: TileId,
590 insertion_point: InsertionPoint,
591 reflow_grid: bool,
592 ) {
593 log::trace!(
594 "Moving {moved_tile_id:?} into {:?}",
595 insertion_point.insertion
596 );
597
598 if let Some((prev_parent_id, source_index)) = self.remove_tile_id_from_parent(moved_tile_id)
599 {
600 if prev_parent_id == insertion_point.parent_id {
603 let parent_tile = self.tiles.get_mut(prev_parent_id);
604
605 if let Some(Tile::Container(container)) = parent_tile {
606 if container.kind() == insertion_point.insertion.kind() {
607 let dest_index = insertion_point.insertion.index();
608 log::trace!(
609 "Moving within the same parent: {source_index} -> {dest_index}"
610 );
611 let adjusted_index = if source_index < dest_index {
614 dest_index - 1
616 } else {
617 dest_index
618 };
619
620 match container {
621 Container::Tabs(tabs) => {
622 let insertion_index = adjusted_index.min(tabs.children.len());
623 tabs.children.insert(insertion_index, moved_tile_id);
624 tabs.active = Some(moved_tile_id);
625 }
626 Container::Linear(linear) => {
627 let insertion_index = adjusted_index.min(linear.children.len());
628 linear.children.insert(insertion_index, moved_tile_id);
629 }
630 Container::Grid(grid) => {
631 if reflow_grid {
632 self.tiles.insert_at(insertion_point, moved_tile_id);
633 } else {
634 let dest_tile = grid.replace_at(dest_index, moved_tile_id);
635 if let Some(dest) = dest_tile {
636 grid.insert_at(source_index, dest);
637 }
638 };
639 }
640 }
641 return; }
643 }
644 }
645 }
646
647 self.tiles.insert_at(insertion_point, moved_tile_id);
649 }
650
651 pub fn dragged_id(&self, ctx: &egui::Context) -> Option<TileId> {
653 for tile_id in self.tiles.tile_ids() {
654 if self.is_root(tile_id) {
655 continue; }
657
658 let is_tile_being_dragged = crate::is_being_dragged(ctx, self.id, tile_id);
659 if is_tile_being_dragged {
660 if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
662 ctx.stop_dragging();
663 return None;
664 }
665
666 return Some(tile_id);
667 }
668 }
669 None
670 }
671
672 pub(super) fn remove_tile_id_from_parent(
680 &mut self,
681 remove_me: TileId,
682 ) -> Option<(TileId, usize)> {
683 let mut result = None;
684
685 for (parent_id, parent) in self.tiles.iter_mut() {
686 if let Tile::Container(container) = parent {
687 if let Some(child_index) = container.remove_child(remove_me) {
688 result = Some((*parent_id, child_index));
689 }
690 }
691 }
692
693 if let Some((parent_id, _)) = result {
698 if let Some(mut tile) = self.tiles.remove(parent_id) {
699 if let Tile::Container(Container::Tabs(tabs)) = &mut tile {
700 tabs.ensure_active(&self.tiles);
701 }
702 self.tiles.insert(parent_id, tile);
703 }
704 }
705
706 result
707 }
708}
709
710fn smooth_preview_rect_id(dragged_tile_id: TileId) -> egui::Id {
715 egui::Id::new((dragged_tile_id, "smoothed_preview_rect"))
716}
717
718fn clear_smooth_preview_rect(ctx: &egui::Context, dragged_tile_id: TileId) {
719 let data_id = smooth_preview_rect_id(dragged_tile_id);
720 ctx.data_mut(|data| data.remove::<Rect>(data_id));
721}
722
723fn smooth_preview_rect(ctx: &egui::Context, dragged_tile_id: TileId, new_rect: Rect) -> Rect {
725 let data_id = smooth_preview_rect_id(dragged_tile_id);
726
727 let dt = ctx.input(|input| input.stable_dt).at_most(0.1);
728
729 let mut requires_repaint = false;
730
731 let smoothed = ctx.data_mut(|data| {
732 let smoothed: &mut Rect = data.get_temp_mut_or(data_id, new_rect);
733
734 let t = egui::emath::exponential_smooth_factor(0.9, 0.05, dt);
735
736 *smoothed = smoothed.lerp_towards(&new_rect, t);
737
738 let diff = smoothed.min.distance(new_rect.min) + smoothed.max.distance(new_rect.max);
739 if diff < 0.5 {
740 *smoothed = new_rect;
741 } else {
742 requires_repaint = true;
743 }
744 *smoothed
745 });
746
747 if requires_repaint {
748 ctx.request_repaint();
749 }
750
751 smoothed
752}