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