1use 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
22pub(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 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 #[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 fn add_popup(
57 &mut self,
58 cx: &mut ConfigCx,
59 data: &Self::Data,
60 id: WindowId,
61 popup: PopupDescriptor,
62 );
63
64 fn remove_popup(&mut self, cx: &mut ConfigCx, id: WindowId);
68
69 fn resize_popups(&mut self, cx: &mut ConfigCx, data: &Self::Data);
74}
75
76pub(crate) struct Properties {
78 icon: Option<Icon>, 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 pub(crate) fn icon(&self) -> Option<Icon> {
108 self.icon.clone()
109 }
110
111 pub fn decorations(&self) -> Decorations {
113 self.decorations
114 }
115
116 pub fn restrictions(&self) -> (bool, bool) {
118 self.restrictions
119 }
120
121 pub fn transparent(&self) -> bool {
123 self.transparent
124 }
125}
126
127pub struct BoxedWindow<Data: 'static>(pub(crate) Box<dyn WindowWidget<Data = Data>>);
129
130#[impl_self]
131mod Window {
132 #[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 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 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 for (_, popup, translation) in &self.popups {
269 if let Some(child) = self.find_tile(&popup.id) {
270 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 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; }
377 self.props.icon = icon;
378 }
379 }
380
381 fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) {
382 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 pub(crate) fn map_any<Data: AppData>(self) -> Window<Data> {
426 Window {
427 core: Default::default(),
428 props: self.props,
429 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 pub fn new(ui: impl Widget<Data = Data> + 'static, title: impl ToString) -> Self {
452 Self::new_boxed(Box::new(ui), title)
453 }
454
455 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 #[inline]
480 pub fn boxed(self) -> BoxedWindow<Data> {
481 BoxedWindow(Box::new(self))
482 }
483
484 pub fn title(&self) -> &str {
486 self.title_bar.title()
487 }
488
489 pub fn with_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
493 self.props.icon = icon.into();
494 self
495 }
496
497 pub fn decorations(&self) -> Decorations {
499 self.props.decorations
500 }
501
502 pub fn with_decorations(mut self, decorations: Decorations) -> Self {
513 self.props.decorations = decorations;
514 self
515 }
516
517 pub fn restrictions(&self) -> (bool, bool) {
519 self.props.restrictions
520 }
521
522 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 pub fn drag_anywhere(&self) -> bool {
548 self.props.drag_anywhere
549 }
550
551 pub fn with_drag_anywhere(mut self, drag_anywhere: bool) -> Self {
556 self.props.drag_anywhere = drag_anywhere;
557 self
558 }
559
560 pub fn transparent(&self) -> bool {
562 self.props.transparent
563 }
564
565 pub fn with_transparent(mut self, transparent: bool) -> Self {
577 self.props.transparent = transparent;
578 self
579 }
580
581 pub fn escapable(mut self) -> Self {
583 self.props.escapable = true;
584 self
585 }
586
587 pub fn with_alt_bypass(mut self) -> Self {
592 self.props.alt_bypass = true;
593 self
594 }
595
596 pub fn without_nav_focus(mut self) -> Self {
601 self.props.disable_nav_focus = true;
602 self
603 }
604
605 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 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; 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 assert!(result.is_some());
729 }
730}