1use 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#[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 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 #[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 fn add_popup(
60 &mut self,
61 cx: &mut SizeCx,
62 data: &Self::Data,
63 id: WindowId,
64 popup: PopupDescriptor,
65 );
66
67 fn remove_popup(&mut self, cx: &mut SizeCx, id: WindowId);
71
72 fn resize_popups(&mut self, cx: &mut SizeCx, data: &Self::Data);
77}
78
79pub(crate) struct Properties {
81 icon: Option<Icon>, 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 pub(crate) fn icon(&self) -> Option<Icon> {
111 self.icon.clone()
112 }
113
114 pub fn decorations(&self) -> Decorations {
116 self.decorations
117 }
118
119 pub fn restrictions(&self) -> (bool, bool) {
121 self.restrictions
122 }
123
124 pub fn transparent(&self) -> bool {
126 self.transparent
127 }
128}
129
130pub struct BoxedWindow<Data: 'static>(pub(crate) Box<dyn WindowWidget<Data = Data>>);
132
133#[impl_self]
134mod Window {
135 #[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 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 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 for (_, popup, translation) in &self.popups {
276 if let Some(child) = self.find_tile(&popup.id) {
277 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 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; }
384 self.props.icon = icon;
385 }
386 }
387
388 fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) {
389 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 pub(crate) fn map_any<Data: AppData>(self) -> Window<Data> {
433 Window {
434 core: Default::default(),
435 props: self.props,
436 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 pub fn new(ui: impl Widget<Data = Data> + 'static, title: impl ToString) -> Self {
460 Self::new_boxed(Box::new(ui), title)
461 }
462
463 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 #[inline]
489 pub fn boxed(self) -> BoxedWindow<Data> {
490 BoxedWindow(Box::new(self))
491 }
492
493 pub fn title(&self) -> &str {
495 self.title_bar.title()
496 }
497
498 pub fn with_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
502 self.props.icon = icon.into();
503 self
504 }
505
506 pub fn decorations(&self) -> Decorations {
508 self.props.decorations
509 }
510
511 pub fn with_decorations(mut self, decorations: Decorations) -> Self {
522 self.props.decorations = decorations;
523 self
524 }
525
526 pub fn restrictions(&self) -> (bool, bool) {
528 self.props.restrictions
529 }
530
531 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 pub fn drag_anywhere(&self) -> bool {
557 self.props.drag_anywhere
558 }
559
560 pub fn with_drag_anywhere(mut self, drag_anywhere: bool) -> Self {
565 self.props.drag_anywhere = drag_anywhere;
566 self
567 }
568
569 pub fn transparent(&self) -> bool {
571 self.props.transparent
572 }
573
574 pub fn with_transparent(mut self, transparent: bool) -> Self {
586 self.props.transparent = transparent;
587 self
588 }
589
590 pub fn escapable(mut self) -> Self {
592 self.props.escapable = true;
593 self
594 }
595
596 pub fn with_alt_bypass(mut self) -> Self {
601 self.props.alt_bypass = true;
602 self
603 }
604
605 pub fn without_nav_focus(mut self) -> Self {
610 self.props.disable_nav_focus = true;
611 self
612 }
613
614 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 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; 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 assert!(result.is_some());
739 }
740}