kas_core/
root.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 crate::cast::Cast;
9use crate::decorations::{Border, Decorations, TitleBar};
10use crate::dir::Directional;
11use crate::event::{ConfigCx, Event, EventCx, IsUsed, ResizeDirection, Scroll, Unused, Used};
12use crate::geom::{Coord, Offset, Rect, Size};
13use crate::layout::{self, AlignHints, AxisInfo, SizeRules};
14use crate::theme::{DrawCx, FrameStyle, SizeCx};
15use crate::{Action, Events, Icon, Id, Layout, LayoutExt, Widget};
16use kas_macros::impl_scope;
17use smallvec::SmallVec;
18use std::num::NonZeroU32;
19
20/// Identifier for a window or pop-up
21///
22/// Identifiers should always be unique.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
24pub struct WindowId(NonZeroU32);
25
26impl WindowId {
27    /// Construct a [`WindowId`]
28    ///
29    /// Only for use by the graphics/platform backend!
30    #[allow(unused)]
31    pub(crate) fn new(n: NonZeroU32) -> WindowId {
32        WindowId(n)
33    }
34
35    pub(crate) fn get(self) -> u32 {
36        self.0.get()
37    }
38}
39
40/// Commands supported by the [`Window`]
41///
42/// This may be sent as a message from any widget in the window.
43#[derive(Clone, Debug)]
44pub enum WindowCommand {
45    /// Change the window's title
46    SetTitle(String),
47    /// Change the window's icon
48    SetIcon(Option<Icon>),
49}
50
51impl_scope! {
52    /// The window widget
53    ///
54    /// This widget is the root of any UI tree used as a window. It manages
55    /// window decorations.
56    ///
57    /// To change window properties at run-time, send a [`WindowCommand`] from a
58    /// child widget.
59    #[widget]
60    pub struct Window<Data: 'static> {
61        core: widget_core!(),
62        icon: Option<Icon>, // initial icon, if any
63        decorations: Decorations,
64        restrictions: (bool, bool),
65        drag_anywhere: bool,
66        transparent: bool,
67        #[widget]
68        inner: Box<dyn Widget<Data = Data>>,
69        #[widget(&())]
70        title_bar: TitleBar,
71        #[widget(&())] b_w: Border,
72        #[widget(&())] b_e: Border,
73        #[widget(&())] b_n: Border,
74        #[widget(&())] b_s: Border,
75        #[widget(&())] b_nw: Border,
76        #[widget(&())] b_ne: Border,
77        #[widget(&())] b_sw: Border,
78        #[widget(&())] b_se: Border,
79        bar_h: i32,
80        dec_offset: Offset,
81        dec_size: Size,
82        popups: SmallVec<[(WindowId, kas::PopupDescriptor, Offset); 16]>,
83    }
84
85    impl Layout for Self {
86        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
87            let mut inner = self.inner.size_rules(sizer.re(), axis);
88
89            self.bar_h = 0;
90            if matches!(self.decorations, Decorations::Toolkit) {
91                let bar = self.title_bar.size_rules(sizer.re(), axis);
92                if axis.is_horizontal() {
93                    inner.max_with(bar);
94                } else {
95                    inner.append(bar);
96                    self.bar_h = bar.min_size();
97                }
98            }
99
100            // These methods don't return anything useful, but we are required to call them:
101            let _ = self.b_w.size_rules(sizer.re(), axis);
102            let _ = self.b_e.size_rules(sizer.re(), axis);
103            let _ = self.b_n.size_rules(sizer.re(), axis);
104            let _ = self.b_s.size_rules(sizer.re(), axis);
105            let _ = self.b_nw.size_rules(sizer.re(), axis);
106            let _ = self.b_ne.size_rules(sizer.re(), axis);
107            let _ = self.b_se.size_rules(sizer.re(), axis);
108            let _ = self.b_sw.size_rules(sizer.re(), axis);
109
110            if matches!(self.decorations, Decorations::Border | Decorations::Toolkit) {
111                let frame = sizer.frame(FrameStyle::Window, axis);
112                let (rules, offset, size) = frame.surround(inner);
113                self.dec_offset.set_component(axis, offset);
114                self.dec_size.set_component(axis, size);
115                rules
116            } else {
117                inner
118            }
119        }
120
121        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
122            self.core.rect = rect;
123            // Calculate position and size for nw, ne, and inner portions:
124            let s_nw: Size = self.dec_offset.cast();
125            let s_se = self.dec_size - s_nw;
126            let mut s_in = rect.size - self.dec_size;
127            let p_nw = rect.pos;
128            let mut p_in = p_nw + self.dec_offset;
129            let p_se = p_in + s_in;
130
131            self.b_w.set_rect(cx, Rect::new(Coord(p_nw.0, p_in.1), Size(s_nw.0, s_in.1)), hints);
132            self.b_e.set_rect(cx, Rect::new(Coord(p_se.0, p_in.1), Size(s_se.0, s_in.1)), hints);
133            self.b_n.set_rect(cx, Rect::new(Coord(p_in.0, p_nw.1), Size(s_in.0, s_nw.1)), hints);
134            self.b_s.set_rect(cx, Rect::new(Coord(p_in.0, p_se.1), Size(s_in.0, s_se.1)), hints);
135            self.b_nw.set_rect(cx, Rect::new(p_nw, s_nw), hints);
136            self.b_ne.set_rect(cx, Rect::new(Coord(p_se.0, p_nw.1), Size(s_se.0, s_nw.1)), hints);
137            self.b_se.set_rect(cx, Rect::new(p_se, s_se), hints);
138            self.b_sw.set_rect(cx, Rect::new(Coord(p_nw.0, p_se.1), Size(s_nw.0, s_se.1)), hints);
139
140            if self.bar_h > 0 {
141                let bar_size = Size(s_in.0, self.bar_h);
142                self.title_bar.set_rect(cx, Rect::new(p_in, bar_size), hints);
143                p_in.1 += self.bar_h;
144                s_in -= Size(0, self.bar_h);
145            }
146            self.inner.set_rect(cx, Rect::new(p_in, s_in), hints);
147        }
148
149        fn find_id(&mut self, _: Coord) -> Option<Id> {
150            unimplemented!()
151        }
152
153        fn draw(&mut self, _: DrawCx) {
154            unimplemented!()
155        }
156    }
157
158    impl Self {
159        pub(crate) fn find_id(&mut self, data: &Data, coord: Coord) -> Option<Id> {
160            if !self.core.rect.contains(coord) {
161                return None;
162            }
163            for (_, popup, translation) in self.popups.iter_mut().rev() {
164                if let Some(Some(id)) = self.inner.as_node(data).find_node(&popup.id, |mut node| node.find_id(coord + *translation)) {
165                    return Some(id);
166                }
167            }
168            if self.bar_h > 0 {
169                if let Some(id) = self.title_bar.find_id(coord) {
170                    return Some(id);
171                }
172            }
173            self.inner.find_id(coord)
174                .or_else(|| self.b_w.find_id(coord))
175                .or_else(|| self.b_e.find_id(coord))
176                .or_else(|| self.b_n.find_id(coord))
177                .or_else(|| self.b_s.find_id(coord))
178                .or_else(|| self.b_nw.find_id(coord))
179                .or_else(|| self.b_ne.find_id(coord))
180                .or_else(|| self.b_sw.find_id(coord))
181                .or_else(|| self.b_se.find_id(coord))
182                .or_else(|| Some(self.id()))
183        }
184
185        #[cfg(winit)]
186        pub(crate) fn draw(&mut self, data: &Data, mut draw: DrawCx) {
187            if self.dec_size != Size::ZERO {
188                draw.frame(self.core.rect, FrameStyle::Window, Default::default());
189                if self.bar_h > 0 {
190                    draw.recurse(&mut self.title_bar);
191                }
192            }
193            draw.recurse(&mut self.inner);
194            for (_, popup, translation) in &self.popups {
195                self.inner.as_node(data).find_node(&popup.id, |mut node| {
196                    let clip_rect = node.rect() - *translation;
197                    draw.with_overlay(clip_rect, *translation, |draw| {
198                        node._draw(draw);
199                    });
200                });
201            }
202        }
203    }
204
205    impl Events for Self {
206        type Data = Data;
207
208        fn configure(&mut self, cx: &mut ConfigCx) {
209            if cx.platform().is_wayland() && self.decorations == Decorations::Server {
210                // Wayland's base protocol does not support server-side decorations
211                // TODO: Wayland has extensions for this; server-side is still
212                // usually preferred where supported (e.g. KDE).
213                self.decorations = Decorations::Toolkit;
214            }
215        }
216
217        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
218            match event {
219                Event::PressStart { .. } if self.drag_anywhere => {
220                    cx.drag_window();
221                    Used
222                }
223                _ => Unused,
224            }
225        }
226
227        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
228            if let Some(cmd) = cx.try_pop() {
229                match cmd {
230                    WindowCommand::SetTitle(title) => {
231                        cx.action(self.id(), self.title_bar.set_title(title));
232                        #[cfg(winit)]
233                        if self.decorations == Decorations::Server {
234                            if let Some(w) = cx.winit_window() {
235                                w.set_title(self.title());
236                            }
237                        }
238                    }
239                    WindowCommand::SetIcon(icon) => {
240                        #[cfg(winit)]
241                        if self.decorations == Decorations::Server {
242                            if let Some(w) = cx.winit_window() {
243                                w.set_window_icon(icon);
244                                return; // do not set self.icon
245                            }
246                        }
247                        self.icon = icon;
248                    }
249                }
250            }
251        }
252
253        fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) {
254            // Something was scrolled; update pop-up translations
255            self.resize_popups(&mut cx.config_cx(), data);
256        }
257    }
258
259    impl std::fmt::Debug for Self {
260        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261            f.debug_struct("Window")
262                .field("core", &self.core)
263                .field("title", &self.title_bar.title())
264                .finish()
265        }
266    }
267}
268
269impl<Data: 'static> Window<Data> {
270    /// Construct a window with a `W: Widget` and a title
271    pub fn new(ui: impl Widget<Data = Data> + 'static, title: impl ToString) -> Self {
272        Self::new_boxed(Box::new(ui), title)
273    }
274
275    /// Construct a window from a boxed `ui` widget and a `title`
276    pub fn new_boxed(ui: Box<dyn Widget<Data = Data>>, title: impl ToString) -> Self {
277        Window {
278            core: Default::default(),
279            icon: None,
280            decorations: Decorations::Server,
281            restrictions: (true, false),
282            drag_anywhere: true,
283            transparent: false,
284            inner: ui,
285            title_bar: TitleBar::new(title),
286            b_w: Border::new(ResizeDirection::West),
287            b_e: Border::new(ResizeDirection::East),
288            b_n: Border::new(ResizeDirection::North),
289            b_s: Border::new(ResizeDirection::South),
290            b_nw: Border::new(ResizeDirection::NorthWest),
291            b_ne: Border::new(ResizeDirection::NorthEast),
292            b_sw: Border::new(ResizeDirection::SouthWest),
293            b_se: Border::new(ResizeDirection::SouthEast),
294            bar_h: 0,
295            dec_offset: Default::default(),
296            dec_size: Default::default(),
297            popups: Default::default(),
298        }
299    }
300
301    /// Get the window's title
302    pub fn title(&self) -> &str {
303        self.title_bar.title()
304    }
305
306    /// Get the window's icon, if any
307    pub(crate) fn icon(&mut self) -> Option<Icon> {
308        self.icon.clone()
309    }
310
311    /// Set the window's icon (inline)
312    ///
313    /// Default: `None`
314    pub fn with_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
315        self.icon = icon.into();
316        self
317    }
318
319    /// Get the preference for window decorations
320    pub fn decorations(&self) -> Decorations {
321        self.decorations
322    }
323
324    /// Set the preference for window decorations
325    ///
326    /// "Windowing" platforms (i.e. not mobile or web) usually include a
327    /// title-bar, icons and potentially side borders. These are known as
328    /// **decorations**.
329    ///
330    /// This controls the *preferred* type of decorations. The resulting
331    /// behaviour is platform-dependent.
332    ///
333    /// Default: [`Decorations::Server`].
334    pub fn with_decorations(mut self, decorations: Decorations) -> Self {
335        self.decorations = decorations;
336        self
337    }
338
339    /// Get window resizing restrictions: `(restrict_min, restrict_max)`
340    pub fn restrictions(&self) -> (bool, bool) {
341        self.restrictions
342    }
343
344    /// Whether to limit the maximum size of a window
345    ///
346    /// All widgets' size rules allow calculation of two sizes: the minimum
347    /// size and the ideal size. Windows are initially sized to the ideal size.
348    ///
349    /// If `restrict_min`, the window may not be sized below the minimum size.
350    /// Default value: `true`.
351    ///
352    /// If `restrict_max`, the window may not be sized above the ideal size.
353    /// Default value: `false`.
354    pub fn with_restrictions(mut self, restrict_min: bool, restrict_max: bool) -> Self {
355        self.restrictions = (restrict_min, restrict_max);
356        let resizable = !restrict_min || !restrict_max;
357        self.b_w.set_resizable(resizable);
358        self.b_e.set_resizable(resizable);
359        self.b_n.set_resizable(resizable);
360        self.b_s.set_resizable(resizable);
361        self.b_nw.set_resizable(resizable);
362        self.b_ne.set_resizable(resizable);
363        self.b_se.set_resizable(resizable);
364        self.b_sw.set_resizable(resizable);
365        self
366    }
367
368    /// Get "drag anywhere" state
369    pub fn drag_anywhere(&self) -> bool {
370        self.drag_anywhere
371    }
372
373    /// Whether to allow dragging the window from the background
374    ///
375    /// If true, then any unhandled click+drag in the window may be used to
376    /// drag the window on supported platforms. Default value: `true`.
377    pub fn with_drag_anywhere(mut self, drag_anywhere: bool) -> Self {
378        self.drag_anywhere = drag_anywhere;
379        self
380    }
381
382    /// Get whether this window should use transparent rendering
383    pub fn transparent(&self) -> bool {
384        self.transparent
385    }
386
387    /// Whether the window supports transparency
388    ///
389    /// If true, painting with `alpha < 1.0` makes the background visible.
390    /// Additionally, window draw targets are cleared to transparent. This does
391    /// not stop theme elements from drawing a solid background.
392    ///
393    /// Note: results may vary by platform. Current output does *not* use
394    /// pre-multiplied alpha which *some* platforms expect, thus pixels with
395    /// partial transparency may have incorrect appearance.
396    ///
397    /// Default: `false`.
398    pub fn with_transparent(mut self, transparent: bool) -> Self {
399        self.transparent = transparent;
400        self
401    }
402
403    /// Add a pop-up as a layer in the current window
404    ///
405    /// Each [`crate::Popup`] is assigned a [`WindowId`]; both are passed.
406    pub(crate) fn add_popup(
407        &mut self,
408        cx: &mut EventCx,
409        data: &Data,
410        id: WindowId,
411        popup: kas::PopupDescriptor,
412    ) {
413        let index = self.popups.len();
414        self.popups.push((id, popup, Offset::ZERO));
415        self.resize_popup(&mut cx.config_cx(), data, index);
416        cx.action(Id::ROOT, Action::REDRAW);
417    }
418
419    /// Trigger closure of a pop-up
420    ///
421    /// If the given `id` refers to a pop-up, it should be closed.
422    pub(crate) fn remove_popup(&mut self, cx: &mut EventCx, id: WindowId) {
423        for i in 0..self.popups.len() {
424            if id == self.popups[i].0 {
425                self.popups.remove(i);
426                cx.action(Id::ROOT, Action::REGION_MOVED);
427                return;
428            }
429        }
430    }
431
432    /// Resize popups
433    ///
434    /// This is called immediately after [`Layout::set_rect`] to resize
435    /// existing pop-ups.
436    pub(crate) fn resize_popups(&mut self, cx: &mut ConfigCx, data: &Data) {
437        for i in 0..self.popups.len() {
438            self.resize_popup(cx, data, i);
439        }
440    }
441}
442
443// Search for a widget by `id`. On success, return that widget's [`Rect`] and
444// the translation of its children.
445fn find_rect(widget: &dyn Layout, id: Id, mut translation: Offset) -> Option<(Rect, Offset)> {
446    let mut widget = widget;
447    loop {
448        if widget.eq_id(&id) {
449            if widget.translation() != Offset::ZERO {
450                // Unvalidated: does this cause issues with the parent's event handlers?
451                log::warn!(
452                    "Parent of pop-up {} has non-zero translation",
453                    widget.identify()
454                );
455            }
456
457            let rect = widget.rect();
458            return Some((rect, translation));
459        } else if let Some(child) = widget
460            .find_child_index(&id)
461            .and_then(|i| widget.get_child(i))
462        {
463            translation += widget.translation();
464            widget = child;
465            continue;
466        } else {
467            return None;
468        }
469    }
470}
471
472impl<Data: 'static> Window<Data> {
473    fn resize_popup(&mut self, cx: &mut ConfigCx, data: &Data, index: usize) {
474        // Notation: p=point/coord, s=size, m=margin
475        // r=window/root rect, c=anchor rect
476        let r = self.core.rect;
477        let (_, ref mut popup, ref mut translation) = self.popups[index];
478
479        let is_reversed = popup.direction.is_reversed();
480        let place_in = |rp, rs: i32, cp: i32, cs: i32, ideal, m: (u16, u16)| -> (i32, i32) {
481            let m: (i32, i32) = (m.0.into(), m.1.into());
482            let before: i32 = cp - (rp + m.1);
483            let before = before.max(0);
484            let after = (rs - (cs + before + m.0)).max(0);
485            if after >= ideal {
486                if is_reversed && before >= ideal {
487                    (cp - ideal - m.1, ideal)
488                } else {
489                    (cp + cs + m.0, ideal)
490                }
491            } else if before >= ideal {
492                (cp - ideal - m.1, ideal)
493            } else if before > after {
494                (rp, before)
495            } else {
496                (cp + cs + m.0, after)
497            }
498        };
499        let place_out = |rp, rs, cp: i32, cs, ideal: i32| -> (i32, i32) {
500            let pos = cp.min(rp + rs - ideal).max(rp);
501            let size = ideal.max(cs).min(rs);
502            (pos, size)
503        };
504
505        let (c, t) = find_rect(self.inner.as_layout(), popup.parent.clone(), Offset::ZERO).unwrap();
506        *translation = t;
507        let r = r + t; // work in translated coordinate space
508        let result = self.inner.as_node(data).find_node(&popup.id, |mut node| {
509            let mut cache = layout::SolveCache::find_constraints(node.re(), cx.size_cx());
510            let ideal = cache.ideal(false);
511            let m = cache.margins();
512
513            let rect = if popup.direction.is_horizontal() {
514                let (x, w) = place_in(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0, m.horiz);
515                let (y, h) = place_out(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1);
516                Rect::new(Coord(x, y), Size::new(w, h))
517            } else {
518                let (x, w) = place_out(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0);
519                let (y, h) = place_in(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1, m.vert);
520                Rect::new(Coord(x, y), Size::new(w, h))
521            };
522
523            cache.apply_rect(node.re(), cx, rect, false);
524            cache.print_widget_heirarchy(node.as_layout());
525        });
526
527        // Event handlers expect that the popup's rect is now assigned.
528        // If we were to try recovering we should remove the popup.
529        assert!(result.is_some());
530    }
531}