kas_core/window/
window.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Window widgets
7
8use super::{Decorations, Icon, Popup, PopupDescriptor, ResizeDirection, WindowId};
9use crate::cast::Cast;
10use crate::dir::{Direction, Directional};
11use crate::event::{Command, ConfigCx, Event, EventCx, IsUsed, Scroll, Unused, Used};
12use crate::geom::{Coord, Offset, Rect, Size};
13use crate::layout::{self, Align, AlignHints, AxisInfo, SizeRules};
14use crate::theme::{DrawCx, FrameStyle, SizeCx};
15use crate::widgets::{Border, Label, TitleBar};
16use crate::{Action, Events, Id, Layout, Role, RoleCx, Tile, TileExt, Widget};
17use kas_macros::{impl_self, widget_set_rect};
18use smallvec::SmallVec;
19
20pub(crate) trait WindowErased {
21    fn as_tile(&self) -> &dyn Tile;
22    fn show_tooltip(&mut self, cx: &mut EventCx, id: Id, text: String);
23    fn close_tooltip(&mut self, cx: &mut EventCx);
24}
25
26#[impl_self]
27mod Window {
28    /// The window widget
29    ///
30    /// This widget is the root of any UI tree used as a window. It manages
31    /// window decorations.
32    ///
33    /// # Messages
34    ///
35    /// [`kas::messages::SetWindowTitle`] may be used to set the title.
36    ///
37    /// [`kas::messages::SetWindowIcon`] may be used to set the icon.
38    #[widget]
39    pub struct Window<Data: 'static> {
40        core: widget_core!(),
41        icon: Option<Icon>, // initial icon, if any
42        decorations: Decorations,
43        restrictions: (bool, bool),
44        drag_anywhere: bool,
45        transparent: bool,
46        escapable: bool,
47        alt_bypass: bool,
48        disable_nav_focus: bool,
49        #[widget]
50        inner: Box<dyn Widget<Data = Data>>,
51        #[widget(&())]
52        tooltip: Popup<Label<String>>,
53        #[widget(&())]
54        title_bar: TitleBar,
55        #[widget(&())]
56        b_w: Border,
57        #[widget(&())]
58        b_e: Border,
59        #[widget(&())]
60        b_n: Border,
61        #[widget(&())]
62        b_s: Border,
63        #[widget(&())]
64        b_nw: Border,
65        #[widget(&())]
66        b_ne: Border,
67        #[widget(&())]
68        b_sw: Border,
69        #[widget(&())]
70        b_se: Border,
71        bar_h: i32,
72        dec_offset: Offset,
73        dec_size: Size,
74        popups: SmallVec<[(WindowId, PopupDescriptor, Offset); 16]>,
75    }
76
77    impl Layout for Self {
78        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
79            let mut inner = self.inner.size_rules(sizer.re(), axis);
80
81            self.bar_h = 0;
82            if matches!(self.decorations, Decorations::Toolkit) {
83                let bar = self.title_bar.size_rules(sizer.re(), axis);
84                if axis.is_horizontal() {
85                    inner.max_with(bar);
86                } else {
87                    inner.append(bar);
88                    self.bar_h = bar.min_size();
89                }
90            }
91
92            // These methods don't return anything useful, but we are required to call them:
93            let _ = self.b_w.size_rules(sizer.re(), axis);
94            let _ = self.b_e.size_rules(sizer.re(), axis);
95            let _ = self.b_n.size_rules(sizer.re(), axis);
96            let _ = self.b_s.size_rules(sizer.re(), axis);
97            let _ = self.b_nw.size_rules(sizer.re(), axis);
98            let _ = self.b_ne.size_rules(sizer.re(), axis);
99            let _ = self.b_se.size_rules(sizer.re(), axis);
100            let _ = self.b_sw.size_rules(sizer.re(), axis);
101
102            if matches!(self.decorations, Decorations::Border | Decorations::Toolkit) {
103                let frame = sizer.frame(FrameStyle::Window, axis);
104                let (rules, offset, size) = frame.surround(inner);
105                self.dec_offset.set_component(axis, offset);
106                self.dec_size.set_component(axis, size);
107                rules
108            } else {
109                inner
110            }
111        }
112
113        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
114            widget_set_rect!(rect);
115            // Calculate position and size for nw, ne, and inner portions:
116            let s_nw: Size = self.dec_offset.cast();
117            let s_se = self.dec_size - s_nw;
118            let mut s_in = rect.size - self.dec_size;
119            let p_nw = rect.pos;
120            let mut p_in = p_nw + self.dec_offset;
121            let p_se = p_in + s_in;
122
123            self.b_w.set_rect(
124                cx,
125                Rect::new(Coord(p_nw.0, p_in.1), Size(s_nw.0, s_in.1)),
126                hints,
127            );
128            self.b_e.set_rect(
129                cx,
130                Rect::new(Coord(p_se.0, p_in.1), Size(s_se.0, s_in.1)),
131                hints,
132            );
133            self.b_n.set_rect(
134                cx,
135                Rect::new(Coord(p_in.0, p_nw.1), Size(s_in.0, s_nw.1)),
136                hints,
137            );
138            self.b_s.set_rect(
139                cx,
140                Rect::new(Coord(p_in.0, p_se.1), Size(s_in.0, s_se.1)),
141                hints,
142            );
143            self.b_nw.set_rect(cx, Rect::new(p_nw, s_nw), hints);
144            self.b_ne.set_rect(
145                cx,
146                Rect::new(Coord(p_se.0, p_nw.1), Size(s_se.0, s_nw.1)),
147                hints,
148            );
149            self.b_se.set_rect(cx, Rect::new(p_se, s_se), hints);
150            self.b_sw.set_rect(
151                cx,
152                Rect::new(Coord(p_nw.0, p_se.1), Size(s_nw.0, s_se.1)),
153                hints,
154            );
155
156            if self.bar_h > 0 {
157                let bar_size = Size(s_in.0, self.bar_h);
158                self.title_bar
159                    .set_rect(cx, Rect::new(p_in, bar_size), hints);
160                p_in.1 += self.bar_h;
161                s_in -= Size(0, self.bar_h);
162            }
163            self.inner.set_rect(cx, Rect::new(p_in, s_in), hints);
164        }
165
166        fn draw(&self, mut draw: DrawCx) {
167            // Draw access keys first to prioritise their access key bindings
168            for (_, popup, translation) in &self.popups {
169                if let Some(child) = self.find_tile(&popup.id) {
170                    // We use a new pass to control draw order and clip content:
171                    let clip_rect = child.rect() - *translation;
172                    draw.with_overlay(clip_rect, *translation, |draw| {
173                        child.draw(draw);
174                    });
175                }
176            }
177
178            if self.dec_size != Size::ZERO {
179                draw.frame(self.rect(), FrameStyle::Window, Default::default());
180                if self.bar_h > 0 {
181                    self.title_bar.draw(draw.re());
182                }
183            }
184            self.inner.draw(draw.re());
185        }
186    }
187
188    impl Tile for Self {
189        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
190            Role::Window
191        }
192
193        fn probe(&self, coord: Coord) -> Id {
194            for (_, popup, translation) in self.popups.iter().rev() {
195                if let Some(widget) = self.inner.find_tile(&popup.id)
196                    && let Some(id) = widget.try_probe(coord + *translation)
197                {
198                    return id;
199                }
200            }
201            if self.bar_h > 0
202                && let Some(id) = self.title_bar.try_probe(coord)
203            {
204                return id;
205            }
206            self.inner
207                .try_probe(coord)
208                .or_else(|| self.b_w.try_probe(coord))
209                .or_else(|| self.b_e.try_probe(coord))
210                .or_else(|| self.b_n.try_probe(coord))
211                .or_else(|| self.b_s.try_probe(coord))
212                .or_else(|| self.b_nw.try_probe(coord))
213                .or_else(|| self.b_ne.try_probe(coord))
214                .or_else(|| self.b_sw.try_probe(coord))
215                .or_else(|| self.b_se.try_probe(coord))
216                .unwrap_or_else(|| self.id())
217        }
218    }
219
220    impl Events for Self {
221        type Data = Data;
222
223        fn configure(&mut self, cx: &mut ConfigCx) {
224            if cx.platform().is_wayland() && self.decorations == Decorations::Server {
225                // Wayland's base protocol does not support server-side decorations
226                // TODO: Wayland has extensions for this; server-side is still
227                // usually preferred where supported (e.g. KDE).
228                self.decorations = Decorations::Toolkit;
229            }
230
231            if self.alt_bypass {
232                cx.config.alt_bypass = true;
233            }
234
235            if self.disable_nav_focus {
236                cx.config.nav_focus = false;
237            }
238        }
239
240        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
241            match event {
242                Event::Command(Command::Escape, _) => {
243                    if let Some(id) = self.popups.last().map(|desc| desc.0) {
244                        cx.close_window(id);
245                    } else if self.escapable {
246                        cx.window_action(Action::CLOSE);
247                    }
248                    Used
249                }
250                Event::PressStart(_) if self.drag_anywhere => {
251                    cx.drag_window();
252                    Used
253                }
254                Event::Timer(handle) if handle == crate::event::Mouse::TIMER_HOVER => {
255                    cx.hover_timer_expiry(self);
256                    Used
257                }
258                _ => Unused,
259            }
260        }
261
262        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
263            if let Some(kas::messages::SetWindowTitle(title)) = cx.try_pop() {
264                self.title_bar.set_title(cx, title);
265                if self.decorations == Decorations::Server
266                    && let Some(w) = cx.winit_window()
267                {
268                    w.set_title(self.title());
269                }
270            } else if let Some(kas::messages::SetWindowIcon(icon)) = cx.try_pop() {
271                if self.decorations == Decorations::Server
272                    && let Some(w) = cx.winit_window()
273                {
274                    w.set_window_icon(icon);
275                    return; // do not set self.icon
276                }
277                self.icon = icon;
278            }
279        }
280
281        fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) {
282            // Something was scrolled; update pop-up translations
283            self.resize_popups(&mut cx.config_cx(), data);
284        }
285    }
286
287    impl WindowErased for Self {
288        fn as_tile(&self) -> &dyn Tile {
289            self
290        }
291
292        fn show_tooltip(&mut self, cx: &mut EventCx, id: Id, text: String) {
293            self.tooltip.inner.set_string(cx, text);
294            self.tooltip.open(cx, &(), id, false);
295        }
296
297        fn close_tooltip(&mut self, cx: &mut EventCx) {
298            self.tooltip.close(cx);
299        }
300    }
301
302    impl std::fmt::Debug for Self {
303        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304            f.debug_struct("Window")
305                .field("core", &self.core)
306                .field("title", &self.title_bar.title())
307                .finish()
308        }
309    }
310}
311
312impl<Data: 'static> Window<Data> {
313    /// Construct a window with a `W: Widget` and a title
314    pub fn new(ui: impl Widget<Data = Data> + 'static, title: impl ToString) -> Self {
315        Self::new_boxed(Box::new(ui), title)
316    }
317
318    /// Construct a window from a boxed `ui` widget and a `title`
319    pub fn new_boxed(ui: Box<dyn Widget<Data = Data>>, title: impl ToString) -> Self {
320        Window {
321            core: Default::default(),
322            icon: None,
323            decorations: Decorations::Server,
324            restrictions: (true, false),
325            drag_anywhere: true,
326            transparent: false,
327            escapable: false,
328            alt_bypass: false,
329            disable_nav_focus: false,
330            inner: ui,
331            tooltip: Popup::new(Label::default(), Direction::Down).align(Align::Center),
332            title_bar: TitleBar::new(title),
333            b_w: Border::new(ResizeDirection::West),
334            b_e: Border::new(ResizeDirection::East),
335            b_n: Border::new(ResizeDirection::North),
336            b_s: Border::new(ResizeDirection::South),
337            b_nw: Border::new(ResizeDirection::NorthWest),
338            b_ne: Border::new(ResizeDirection::NorthEast),
339            b_sw: Border::new(ResizeDirection::SouthWest),
340            b_se: Border::new(ResizeDirection::SouthEast),
341            bar_h: 0,
342            dec_offset: Default::default(),
343            dec_size: Default::default(),
344            popups: Default::default(),
345        }
346    }
347
348    /// Get the window's title
349    pub fn title(&self) -> &str {
350        self.title_bar.title()
351    }
352
353    /// Get the window's icon, if any
354    pub(crate) fn icon(&mut self) -> Option<Icon> {
355        self.icon.clone()
356    }
357
358    /// Set the window's icon (inline)
359    ///
360    /// Default: `None`
361    pub fn with_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
362        self.icon = icon.into();
363        self
364    }
365
366    /// Get the preference for window decorations
367    pub fn decorations(&self) -> Decorations {
368        self.decorations
369    }
370
371    /// Set the preference for window decorations
372    ///
373    /// "Windowing" platforms (i.e. not mobile or web) usually include a
374    /// title-bar, icons and potentially side borders. These are known as
375    /// **decorations**.
376    ///
377    /// This controls the *preferred* type of decorations. The resulting
378    /// behaviour is platform-dependent.
379    ///
380    /// Default: [`Decorations::Server`].
381    pub fn with_decorations(mut self, decorations: Decorations) -> Self {
382        self.decorations = decorations;
383        self
384    }
385
386    /// Get window resizing restrictions: `(restrict_min, restrict_max)`
387    pub fn restrictions(&self) -> (bool, bool) {
388        self.restrictions
389    }
390
391    /// Whether to limit the maximum size of a window
392    ///
393    /// All widgets' size rules allow calculation of two sizes: the minimum
394    /// size and the ideal size. Windows are initially sized to the ideal size.
395    ///
396    /// If `restrict_min`, the window may not be sized below the minimum size.
397    /// Default value: `true`.
398    ///
399    /// If `restrict_max`, the window may not be sized above the ideal size.
400    /// Default value: `false`.
401    pub fn with_restrictions(mut self, restrict_min: bool, restrict_max: bool) -> Self {
402        self.restrictions = (restrict_min, restrict_max);
403        let resizable = !restrict_min || !restrict_max;
404        self.b_w.set_resizable(resizable);
405        self.b_e.set_resizable(resizable);
406        self.b_n.set_resizable(resizable);
407        self.b_s.set_resizable(resizable);
408        self.b_nw.set_resizable(resizable);
409        self.b_ne.set_resizable(resizable);
410        self.b_se.set_resizable(resizable);
411        self.b_sw.set_resizable(resizable);
412        self
413    }
414
415    /// Get "drag anywhere" state
416    pub fn drag_anywhere(&self) -> bool {
417        self.drag_anywhere
418    }
419
420    /// Whether to allow dragging the window from the background
421    ///
422    /// If true, then any unhandled click+drag in the window may be used to
423    /// drag the window on supported platforms. Default value: `true`.
424    pub fn with_drag_anywhere(mut self, drag_anywhere: bool) -> Self {
425        self.drag_anywhere = drag_anywhere;
426        self
427    }
428
429    /// Get whether this window should use transparent rendering
430    pub fn transparent(&self) -> bool {
431        self.transparent
432    }
433
434    /// Whether the window supports transparency
435    ///
436    /// If true, painting with `alpha < 1.0` makes the background visible.
437    /// Additionally, window draw targets are cleared to transparent. This does
438    /// not stop theme elements from drawing a solid background.
439    ///
440    /// Note: results may vary by platform. Current output does *not* use
441    /// pre-multiplied alpha which *some* platforms expect, thus pixels with
442    /// partial transparency may have incorrect appearance.
443    ///
444    /// Default: `false`.
445    pub fn with_transparent(mut self, transparent: bool) -> Self {
446        self.transparent = transparent;
447        self
448    }
449
450    /// Enable closure via <kbd>Escape</kbd> key
451    pub fn escapable(mut self) -> Self {
452        self.escapable = true;
453        self
454    }
455
456    /// Enable <kbd>Alt</kbd> bypass
457    ///
458    /// Access keys usually require that <kbd>Alt</kbd> be held. This method
459    /// allows access keys to be activated without holding <kbd>Alt</kbd>.
460    pub fn with_alt_bypass(mut self) -> Self {
461        self.alt_bypass = true;
462        self
463    }
464
465    /// Disable navigation focus
466    ///
467    /// Usually, widgets may be focussed and this focus may be navigated using
468    /// the <kbd>Tab</kbd> key. This method prevents widgets from gaining focus.
469    pub fn without_nav_focus(mut self) -> Self {
470        self.disable_nav_focus = true;
471        self
472    }
473
474    /// Add a pop-up as a layer in the current window
475    ///
476    /// Each [`crate::Popup`] is assigned a [`WindowId`]; both are passed.
477    pub(crate) fn add_popup(
478        &mut self,
479        cx: &mut ConfigCx,
480        data: &Data,
481        id: WindowId,
482        popup: PopupDescriptor,
483    ) {
484        let index = 'index: {
485            for i in 0..self.popups.len() {
486                if self.popups[i].0 == id {
487                    debug_assert_eq!(self.popups[i].1.id, popup.id);
488                    self.popups[i].1 = popup;
489                    break 'index i;
490                }
491            }
492
493            let len = self.popups.len();
494            self.popups.push((id, popup, Offset::ZERO));
495            len
496        };
497
498        self.resize_popup(cx, data, index);
499        cx.confirm_popup_is_sized(id);
500        cx.action(self.id(), Action::REGION_MOVED);
501    }
502
503    /// Trigger closure of a pop-up
504    ///
505    /// If the given `id` refers to a pop-up, it should be closed.
506    pub(crate) fn remove_popup(&mut self, cx: &mut ConfigCx, id: WindowId) {
507        for i in 0..self.popups.len() {
508            if id == self.popups[i].0 {
509                self.popups.remove(i);
510                cx.action(self.id(), Action::REGION_MOVED);
511                return;
512            }
513        }
514    }
515
516    /// Resize popups
517    ///
518    /// This is called immediately after [`Layout::set_rect`] to resize
519    /// existing pop-ups.
520    pub(crate) fn resize_popups(&mut self, cx: &mut ConfigCx, data: &Data) {
521        for i in 0..self.popups.len() {
522            self.resize_popup(cx, data, i);
523        }
524    }
525
526    /// Iterate over popups
527    #[cfg(feature = "accesskit")]
528    pub(crate) fn iter_popups(&self) -> impl Iterator<Item = &PopupDescriptor> {
529        self.popups.iter().map(|(_, popup, _)| popup)
530    }
531}
532
533impl<Data: 'static> Window<Data> {
534    fn resize_popup(&mut self, cx: &mut ConfigCx, data: &Data, index: usize) {
535        // Notation: p=point/coord, s=size, m=margin
536        // r=window/root rect, c=anchor rect
537        let r = self.rect();
538        let popup = self.popups[index].1.clone();
539
540        let is_reversed = popup.direction.is_reversed();
541        let place_in = |rp, rs: i32, cp: i32, cs: i32, ideal, m: (u16, u16)| -> (i32, i32) {
542            let m: (i32, i32) = (m.0.into(), m.1.into());
543            let before: i32 = cp - (rp + m.1);
544            let before = before.max(0);
545            let after = (rs - (cs + before + m.0)).max(0);
546            if after >= ideal {
547                if is_reversed && before >= ideal {
548                    (cp - ideal - m.1, ideal)
549                } else {
550                    (cp + cs + m.0, ideal)
551                }
552            } else if before >= ideal {
553                (cp - ideal - m.1, ideal)
554            } else if before > after {
555                (rp, before)
556            } else {
557                (cp + cs + m.0, after)
558            }
559        };
560        let place_out = |rp, rs, cp: i32, cs, ideal: i32, align| -> (i32, i32) {
561            let mut size = ideal.max(cs).min(rs);
562            let pos = match align {
563                Align::Default | Align::TL => cp,
564                Align::BR => cp + cs,
565                Align::Center => cp + (cs - size) / 2,
566                Align::Stretch => {
567                    size = size.max(cs);
568                    cp
569                }
570            };
571            let pos = pos.min(rp + rs - size).max(rp);
572            (pos, size)
573        };
574
575        let Some((c, t)) = self.as_tile().find_tile_rect(&popup.parent) else {
576            return;
577        };
578        self.popups[index].2 = t;
579        let r = r + t; // work in translated coordinate space
580        let result = self.as_node(data).find_node(&popup.id, |mut node| {
581            let mut cache = layout::SolveCache::find_constraints(node.re(), cx.size_cx());
582            let ideal = cache.ideal(false);
583            let m = cache.margins();
584
585            let rect = if popup.direction.is_horizontal() {
586                let (x, w) = place_in(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0, m.horiz);
587                let (y, h) = place_out(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1, popup.align);
588                Rect::new(Coord(x, y), Size::new(w, h))
589            } else {
590                let (x, w) = place_out(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0, popup.align);
591                let (y, h) = place_in(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1, m.vert);
592                Rect::new(Coord(x, y), Size::new(w, h))
593            };
594
595            cache.apply_rect(node.re(), cx, rect, false);
596            cache.print_widget_heirarchy(node.as_tile());
597        });
598
599        // Event handlers expect that the popup's rect is now assigned.
600        // If we were to try recovering we should remove the popup.
601        assert!(result.is_some());
602    }
603}