Skip to main content

journey/widgets/
shell.rs

1//! A generic flat-focus container — the engine behind both journey screens.
2//!
3//! saudade's focus model is flat *per container*: a focusable widget nested
4//! inside another container collapses to a single Tab stop. So journey keeps
5//! every pane a direct child of one `Shell`, which gives correct Tab cycling
6//! across all panes, lets the menu bar's Alt-accelerators reach it regardless
7//! of which pane has focus, and floats a modal dialog overlay over the whole
8//! window. The event / focus / capture / accelerator / overlay handling
9//! mirrors saudade's `Column`; only the per-child placement differs, which is
10//! why each child carries its own layout closure. The gitk browse screen and
11//! the git-gui commit screen are then just two different sets of placements
12//! (see [`crate::widgets::layout`]).
13
14use saudade::{Color, Event, EventCtx, Painter, PopupRequest, Rect, Theme, Widget};
15
16/// Computes a child's rectangle from the container's bounds.
17type Place = Box<dyn Fn(Rect) -> Rect>;
18
19struct Child {
20    widget: Box<dyn Widget>,
21    place: Place,
22}
23
24pub struct Shell {
25    bounds: Rect,
26    background: Option<Color>,
27    children: Vec<Child>,
28    overlays: Vec<Box<dyn Widget>>,
29    captured: Option<usize>,
30    focused: Option<usize>,
31}
32
33impl Shell {
34    pub fn new() -> Self {
35        Self {
36            bounds: Rect::new(0, 0, 0, 0),
37            background: Some(Color::LIGHT_GRAY),
38            children: Vec::new(),
39            overlays: Vec::new(),
40            captured: None,
41            focused: None,
42        }
43    }
44
45    /// Drop the solid background fill so the window's desktop pattern shows
46    /// through the gaps between panes — the git-gui-style commit screen floats
47    /// its widgets on the patterned background rather than a flat fill.
48    pub fn no_background(mut self) -> Self {
49        self.background = None;
50        self
51    }
52
53    /// Add a child positioned by `place`. Call order also sets the keyboard
54    /// focus order — Tab visits focusable children in the order they're added.
55    pub fn add(
56        mut self,
57        widget: impl Widget + 'static,
58        place: impl Fn(Rect) -> Rect + 'static,
59    ) -> Self {
60        self.children.push(Child {
61            widget: Box::new(widget),
62            place: Box::new(place),
63        });
64        self
65    }
66
67    /// Add a floating overlay (e.g. a modal dialog) over the whole shell.
68    pub fn add_overlay(mut self, widget: impl Widget + 'static) -> Self {
69        self.overlays.push(Box::new(widget));
70        self
71    }
72
73    /// Focus the child at `index` (a direct-child index, not a focusable-only
74    /// index), if it is focusable. Returns whether focus moved there.
75    pub fn focus_child(&mut self, index: usize) -> bool {
76        let Some(child) = self.children.get(index) else {
77            return false;
78        };
79        if !child.widget.focusable() {
80            return false;
81        }
82        if let Some(old) = self.focused
83            && old != index
84            && let Some(c) = self.children.get_mut(old)
85        {
86            c.widget.set_focused(false);
87        }
88        let focused = self.children[index].widget.focus_first();
89        if focused {
90            self.focused = Some(index);
91        }
92        focused
93    }
94
95    fn active_overlay(&self) -> Option<usize> {
96        self.overlays.iter().position(|o| o.captures_pointer())
97    }
98
99    fn choose_target(&self, event: &Event) -> Option<usize> {
100        if event.is_keyboard() {
101            return self.focused;
102        }
103        if let Some(idx) = self.captured {
104            return Some(idx);
105        }
106        let pos = event.position()?;
107        (0..self.children.len())
108            .rev()
109            .find(|&i| self.children[i].widget.bounds().contains(pos))
110    }
111
112    fn change_focus(&mut self, new_focus: Option<usize>, ctx: &mut EventCtx) {
113        if new_focus == self.focused {
114            return;
115        }
116        if let Some(old) = self.focused
117            && let Some(c) = self.children.get_mut(old)
118        {
119            c.widget.set_focused(false);
120        }
121        if let Some(new) = new_focus
122            && let Some(c) = self.children.get_mut(new)
123        {
124            c.widget.focus_first();
125        }
126        self.focused = new_focus;
127        ctx.request_paint();
128    }
129
130    fn focusable_count(&self) -> usize {
131        self.children
132            .iter()
133            .filter(|c| c.widget.focusable())
134            .count()
135    }
136
137    fn cycle_focus(&mut self, dir: i32, ctx: &mut EventCtx) -> bool {
138        let candidates: Vec<usize> = (0..self.children.len())
139            .filter(|&i| self.children[i].widget.focusable())
140            .collect();
141        if candidates.is_empty() {
142            return false;
143        }
144        let cur_pos = self
145            .focused
146            .and_then(|c| candidates.iter().position(|&i| i == c));
147        let n = candidates.len() as i32;
148        let next = match cur_pos {
149            None => {
150                if dir > 0 {
151                    candidates[0]
152                } else {
153                    candidates[(n - 1) as usize]
154                }
155            }
156            Some(p) => candidates[((p as i32 + dir).rem_euclid(n)) as usize],
157        };
158        if Some(next) == self.focused {
159            return false;
160        }
161        self.change_focus(Some(next), ctx);
162        true
163    }
164}
165
166impl Default for Shell {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl Widget for Shell {
173    fn bounds(&self) -> Rect {
174        self.bounds
175    }
176
177    fn layout(&mut self, bounds: Rect) {
178        self.bounds = bounds;
179        for child in &mut self.children {
180            let rect = (child.place)(bounds);
181            child.widget.layout(rect);
182        }
183        for overlay in &mut self.overlays {
184            overlay.layout(bounds);
185        }
186    }
187
188    fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
189        if let Some(background) = self.background {
190            painter.fill_rect(self.bounds, background);
191        }
192        for child in &mut self.children {
193            child.widget.paint(painter, theme);
194        }
195        for child in &mut self.children {
196            child.widget.paint_overlay(painter, theme);
197        }
198        for overlay in &mut self.overlays {
199            overlay.paint(painter, theme);
200            overlay.paint_overlay(painter, theme);
201        }
202    }
203
204    fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
205        for child in &mut self.children {
206            child.widget.paint_overlay(painter, theme);
207        }
208        for overlay in &mut self.overlays {
209            overlay.paint_overlay(painter, theme);
210        }
211    }
212
213    fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
214        if let Some(idx) = self.active_overlay() {
215            self.overlays[idx].event(event, ctx);
216            return;
217        }
218
219        if !event.is_keyboard() && event.position().is_none() && self.captured.is_none() {
220            for child in &mut self.children {
221                child.widget.event(event, ctx);
222            }
223            return;
224        }
225
226        if event.is_keyboard() {
227            let mut accelerator_blocking = false;
228            for (idx, child) in self.children.iter_mut().enumerate() {
229                if child.widget.accepts_accelerators() && Some(idx) != self.focused {
230                    child.widget.event(event, ctx);
231                    if ctx.is_consumed() {
232                        return;
233                    }
234                    if child.widget.captures_pointer() {
235                        accelerator_blocking = true;
236                    }
237                }
238            }
239            if accelerator_blocking {
240                return;
241            }
242
243            match tab_action(event) {
244                Some(TabKind::Cycle(dir)) => {
245                    if self.cycle_focus(dir, ctx) {
246                        return;
247                    }
248                }
249                Some(TabKind::Swallow) if self.focusable_count() >= 2 => return,
250                _ => {}
251            }
252        }
253
254        let Some(idx) = self.choose_target(event) else {
255            return;
256        };
257
258        let captured_was_set = self.captured == Some(idx);
259        {
260            let child = &mut self.children[idx];
261            child.widget.event(event, ctx);
262            if !event.is_keyboard() {
263                if child.widget.captures_pointer() {
264                    self.captured = Some(idx);
265                } else if captured_was_set {
266                    self.captured = None;
267                }
268            }
269        }
270
271        if ctx.is_focus_requested() {
272            ctx.clear_focus_flags();
273            self.change_focus(Some(idx), ctx);
274        } else if ctx.is_focus_released() {
275            ctx.clear_focus_flags();
276            if self.focused == Some(idx) {
277                self.change_focus(None, ctx);
278            }
279        }
280    }
281
282    fn captures_pointer(&self) -> bool {
283        self.captured.is_some() || self.active_overlay().is_some()
284    }
285
286    fn focusable(&self) -> bool {
287        self.children.iter().any(|c| c.widget.focusable())
288    }
289
290    fn focus_first(&mut self) -> bool {
291        for (idx, child) in self.children.iter_mut().enumerate() {
292            if child.widget.focus_first() {
293                self.focused = Some(idx);
294                return true;
295            }
296        }
297        false
298    }
299
300    fn popup_request(&self) -> Option<PopupRequest> {
301        for overlay in &self.overlays {
302            if let Some(req) = overlay.popup_request() {
303                return Some(req);
304            }
305        }
306        for child in &self.children {
307            if let Some(req) = child.widget.popup_request() {
308                return Some(req);
309            }
310        }
311        None
312    }
313
314    fn wants_ticks(&self) -> bool {
315        self.children.iter().any(|c| c.widget.wants_ticks())
316            || self.overlays.iter().any(|o| o.wants_ticks())
317    }
318}
319
320// Tab handling mirrors saudade's internal `tab_action`, which isn't public.
321enum TabKind {
322    Cycle(i32),
323    Swallow,
324}
325
326fn tab_action(event: &Event) -> Option<TabKind> {
327    use saudade::{Key, NamedKey};
328    match event {
329        Event::KeyDown {
330            key: Key::Named(NamedKey::Tab),
331            modifiers,
332        } if !modifiers.control && !modifiers.alt && !modifiers.logo => {
333            Some(TabKind::Cycle(if modifiers.shift { -1 } else { 1 }))
334        }
335        Event::Char {
336            ch: '\t',
337            modifiers,
338        } if !modifiers.control && !modifiers.alt && !modifiers.logo => Some(TabKind::Swallow),
339        _ => None,
340    }
341}