egui_table_kit/layout/split_scroll.rs
1use egui::{Rect, Ui, UiBuilder, Vec2, Vec2b, pos2, vec2};
2/// A scroll area with some portion of its left and/or top side "stuck".
3///
4/// This produces four quadrants:
5///
6/// ```text
7/// <-------LEFT-------> <---------RIGHT---------->
8///
9/// ------------------------------------------------
10/// ^ | | <-----------------> |
11/// TOP | | Fixed | Horizontally |
12/// V | fixed_size | scrollable |
13/// |--------------------|-------------------------|.................
14/// ^ | ^ | ^ | .
15/// BOTTOM | | | Vertically | <- Fully scrollable -> | .
16/// | | | scrollable | scroll_outer_size | .
17/// V | v | v | .
18/// |____________________|_________________________| .
19/// . .
20/// . scroll_content_size .
21/// . .
22/// ............................................
23/// ```
24///
25/// The above shows the initial layout when the scroll offset is zero (no scrolling has occurred yet).
26#[derive(Clone, Copy, Debug)]
27pub struct SplitScroll {
28 pub scroll_enabled: Vec2b,
29
30 /// Width of the fixed left side, and height of the fixed top.
31 pub fixed_size: Vec2,
32
33 /// Size of the small container of the right bottom scrollable region.
34 pub scroll_outer_size: Vec2,
35
36 /// Size of the large contents of the right bottom region, ignoring the left/top fixed regions.
37 pub scroll_content_size: Vec2,
38
39 /// If true, the vertical scrollbar will stick to the bottom as the content grows.
40 pub stick_to_bottom: bool,
41}
42
43/// The contents of a [`SplitScroll`].
44/// The contents of a [`SplitScroll`].
45pub trait SplitScrollDelegate {
46 fn left_top_ui(&mut self, ui: &mut Ui);
47 fn right_top_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2);
48 fn left_bottom_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2);
49 fn right_bottom_ui(&mut self, ui: &mut Ui, scroll_offset: Vec2);
50
51 /// Paints overlays (like separator lines).
52 /// Called *inside* the `ScrollArea` viewport closure (clipped to bottom-right)
53 /// so that scrollbars paint over it.
54 /// Also called *outside* (clipped to left) to paint over sticky areas.
55 fn paint_overlays(&mut self, _ui: &mut Ui) {}
56
57 /// Updates state like auto-sized column widths and drag interaction.
58 /// Called *after* all UI quadrants and `paint_overlays` have been rendered,
59 /// ensuring `max_column_widths` is fully populated and visual lines match the current frame's layout.
60 fn update_col_widths(&mut self, _ui: &mut Ui) {}
61}
62
63impl SplitScroll {
64 pub fn show(self, ui: &mut Ui, delegate: &mut dyn SplitScrollDelegate) {
65 let Self {
66 scroll_enabled,
67 fixed_size,
68 scroll_outer_size,
69 scroll_content_size,
70 stick_to_bottom,
71 } = self;
72
73 ui.scope(|ui| {
74 ui.visuals_mut().clip_rect_margin = 0.0; // Everything else looks awful
75
76 let mut rect = ui.cursor();
77 rect.max = rect.min + fixed_size + scroll_outer_size;
78
79 let total_columns_width = fixed_size.x + scroll_content_size.x;
80 let columns_right_edge = rect.min.x + total_columns_width;
81 let right_boundary = columns_right_edge.min(rect.max.x);
82 rect.max.x = right_boundary;
83
84 ui.shrink_clip_rect(rect);
85 let rect = rect;
86
87 let bottom_right_rect = Rect::from_min_max(rect.min + fixed_size, rect.max);
88
89 let scroll_offset = {
90 let mut scroll_ui = ui.new_child(UiBuilder::new().max_rect(rect));
91
92 egui::ScrollArea::new(scroll_enabled)
93 .auto_shrink(false)
94 .scroll_bar_rect(bottom_right_rect)
95 .stick_to_bottom(stick_to_bottom)
96 .show_viewport(&mut scroll_ui, |ui, scroll_offset_rect| {
97 ui.set_min_size(fixed_size + scroll_content_size);
98 let scroll_offset = scroll_offset_rect.min.to_vec2();
99
100 // 1. RIGHT BOTTOM
101 let mut shrunk_rect = ui.max_rect();
102 shrunk_rect.min += fixed_size;
103 let mut shrunk_ui = ui.new_child(UiBuilder::new().max_rect(shrunk_rect));
104 shrunk_ui.shrink_clip_rect(bottom_right_rect);
105 delegate.right_bottom_ui(&mut shrunk_ui, scroll_offset);
106
107 // 2. PAINT OVERLAYS (Bottom-Right)
108 // Paint lines before scrollbars. Clipped to bottom_right_rect.
109 delegate.paint_overlays(&mut shrunk_ui);
110
111 scroll_offset_rect.min
112 })
113 .inner
114 }
115 .to_vec2();
116
117 {
118 // LEFT TOP: Fixed
119 let left_top_rect = rect
120 .with_max_x(rect.left() + fixed_size.x)
121 .with_max_y(rect.top() + fixed_size.y);
122 let mut left_top_ui = ui.new_child(UiBuilder::new().max_rect(left_top_rect));
123 left_top_ui.shrink_clip_rect(left_top_rect);
124 delegate.left_top_ui(&mut left_top_ui);
125 }
126
127 {
128 // RIGHT TOP: Horizontally scrollable
129 let right_top_outer_rect = rect
130 .with_min_x(rect.left() + fixed_size.x)
131 .with_max_y(rect.top() + fixed_size.y);
132 let right_top_content_rect = Rect::from_min_size(
133 pos2(right_top_outer_rect.min.x - scroll_offset.x, rect.min.y),
134 vec2(scroll_content_size.x, fixed_size.y),
135 );
136 let mut right_top_ui =
137 ui.new_child(UiBuilder::new().max_rect(right_top_content_rect));
138 right_top_ui.shrink_clip_rect(right_top_outer_rect);
139 delegate.right_top_ui(&mut right_top_ui, scroll_offset);
140 }
141
142 {
143 // LEFT BOTTOM: Vertically scrollable
144 let left_bottom_outer_rect = rect
145 .with_max_x(rect.left() + fixed_size.x)
146 .with_min_y(rect.top() + fixed_size.y);
147 let left_bottom_content_rect = Rect::from_min_size(
148 pos2(rect.min.x, left_bottom_outer_rect.min.y - scroll_offset.y),
149 vec2(fixed_size.x, scroll_content_size.y),
150 );
151 let mut left_bottom_ui =
152 ui.new_child(UiBuilder::new().max_rect(left_bottom_content_rect));
153 left_bottom_ui.shrink_clip_rect(left_bottom_outer_rect);
154 delegate.left_bottom_ui(&mut left_bottom_ui, scroll_offset);
155 }
156
157 // 3. PAINT OVERLAYS (Left)
158 // Paint lines over sticky areas. Clipped to left_rect.
159 let left_rect = rect.with_max_x(rect.left() + fixed_size.x);
160 let mut left_ui = ui.new_child(UiBuilder::new().max_rect(left_rect));
161 left_ui.shrink_clip_rect(left_rect);
162 delegate.paint_overlays(&mut left_ui);
163
164 // 4. UPDATE COL WIDTHS
165 // Now that headers and body have populated `max_column_widths`, calculate final sizes.
166 delegate.update_col_widths(ui);
167
168 ui.advance_cursor_after_rect(rect);
169 });
170 }
171}