Skip to main content

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