Skip to main content

egui_components/
resizable.rs

1//! `Resizable` — two panes separated by a draggable divider.
2//!
3//! The split position is persisted in egui memory keyed by the supplied id, so
4//! it survives across frames. Works horizontally (side-by-side, default) or
5//! vertically (stacked).
6//!
7//! ```ignore
8//! sc::Resizable::new("split")
9//!     .default_fraction(0.3)
10//!     .show(ui, |ui| { ui.label("left"); }, |ui| { ui.label("right"); });
11//! ```
12
13use egui::{pos2, Id, Rect, Sense, Ui, UiBuilder};
14use egui_components_theme::{mix, Theme};
15
16pub struct Resizable {
17    id: Id,
18    vertical: bool,
19    default_fraction: f32,
20    min_fraction: f32,
21    max_fraction: f32,
22    handle_thickness: f32,
23}
24
25impl Resizable {
26    pub fn new(id_salt: impl std::hash::Hash) -> Self {
27        Self {
28            id: Id::new(id_salt),
29            vertical: false,
30            default_fraction: 0.5,
31            min_fraction: 0.1,
32            max_fraction: 0.9,
33            handle_thickness: 6.0,
34        }
35    }
36    /// Stack the panes vertically (divider is horizontal) instead of side by side.
37    pub fn vertical(mut self) -> Self {
38        self.vertical = true;
39        self
40    }
41    pub fn default_fraction(mut self, f: f32) -> Self {
42        self.default_fraction = f.clamp(0.05, 0.95);
43        self
44    }
45    pub fn min_fraction(mut self, f: f32) -> Self {
46        self.min_fraction = f;
47        self
48    }
49    pub fn max_fraction(mut self, f: f32) -> Self {
50        self.max_fraction = f;
51        self
52    }
53
54    pub fn show(
55        self,
56        ui: &mut Ui,
57        first: impl FnOnce(&mut Ui),
58        second: impl FnOnce(&mut Ui),
59    ) {
60        let theme = Theme::get(ui.ctx());
61        let c = theme.colors;
62        let mem_id = ui.make_persistent_id(self.id);
63
64        let mut fraction = ui
65            .data(|d| d.get_temp::<f32>(mem_id))
66            .unwrap_or(self.default_fraction);
67
68        // Claim the whole available space.
69        let avail = ui.available_size();
70        let total = if self.vertical { avail.y } else { avail.x };
71        let (rect, _) = ui.allocate_exact_size(avail, Sense::hover());
72        let half = self.handle_thickness * 0.5;
73
74        let split = (total * fraction).round();
75
76        let (first_rect, handle_rect, second_rect) = if self.vertical {
77            (
78                Rect::from_min_max(rect.min, pos2(rect.right(), rect.top() + split - half)),
79                Rect::from_min_max(
80                    pos2(rect.left(), rect.top() + split - half),
81                    pos2(rect.right(), rect.top() + split + half),
82                ),
83                Rect::from_min_max(pos2(rect.left(), rect.top() + split + half), rect.max),
84            )
85        } else {
86            (
87                Rect::from_min_max(rect.min, pos2(rect.left() + split - half, rect.bottom())),
88                Rect::from_min_max(
89                    pos2(rect.left() + split - half, rect.top()),
90                    pos2(rect.left() + split + half, rect.bottom()),
91                ),
92                Rect::from_min_max(pos2(rect.left() + split + half, rect.top()), rect.max),
93            )
94        };
95
96        // Drag handle.
97        let handle = ui.interact(handle_rect, mem_id.with("handle"), Sense::drag());
98        if handle.dragged() && total > 0.0 {
99            let delta = if self.vertical {
100                handle.drag_delta().y
101            } else {
102                handle.drag_delta().x
103            };
104            fraction = (fraction + delta / total).clamp(self.min_fraction, self.max_fraction);
105            ui.data_mut(|d| d.insert_temp(mem_id, fraction));
106        }
107        if handle.hovered() || handle.dragged() {
108            ui.ctx().set_cursor_icon(if self.vertical {
109                egui::CursorIcon::ResizeVertical
110            } else {
111                egui::CursorIcon::ResizeHorizontal
112            });
113        }
114
115        // Divider line + grip.
116        let line_color = if handle.hovered() || handle.dragged() {
117            c.ring
118        } else {
119            c.border
120        };
121        let painter = ui.painter();
122        if self.vertical {
123            let y = handle_rect.center().y;
124            painter.line_segment(
125                [pos2(rect.left(), y), pos2(rect.right(), y)],
126                egui::Stroke::new(1.0, line_color),
127            );
128        } else {
129            let x = handle_rect.center().x;
130            painter.line_segment(
131                [pos2(x, rect.top()), pos2(x, rect.bottom())],
132                egui::Stroke::new(1.0, line_color),
133            );
134        }
135        // A faint grip block under the pointer for affordance.
136        if handle.hovered() || handle.dragged() {
137            painter.rect_filled(
138                handle_rect,
139                theme.corner_sm(),
140                mix(c.ring, c.background, 0.85),
141            );
142        }
143
144        // Pane contents.
145        let mut first_ui = ui.new_child(
146            UiBuilder::new()
147                .max_rect(first_rect.shrink(2.0))
148                .layout(egui::Layout::top_down(egui::Align::Min)),
149        );
150        first(&mut first_ui);
151
152        let mut second_ui = ui.new_child(
153            UiBuilder::new()
154                .max_rect(second_rect.shrink(2.0))
155                .layout(egui::Layout::top_down(egui::Align::Min)),
156        );
157        second(&mut second_ui);
158    }
159}