use super::icon::Icon;
use super::{Decorations, Popup, PopupDescriptor, ResizeDirection, WindowId};
use crate::cast::Cast;
use crate::dir::{Direction, Directional};
use crate::event::{Command, ConfigCx, Event, EventCx, IsUsed, Scroll, Unused, Used};
use crate::geom::{Coord, Offset, Rect, Size};
use crate::layout::{self, Align, AlignHints, AxisInfo, SizeRules};
use crate::runner::AppData;
use crate::theme::{DrawCx, FrameStyle, SizeCx};
use crate::widgets::adapt::MapAny;
use crate::widgets::{Border, Label, TitleBar};
use crate::{Events, Id, Layout, Role, RoleCx, Tile, TileExt, Widget, WidgetCoreRect};
use kas_macros::{autoimpl, impl_self};
use smallvec::SmallVec;
#[cfg(feature = "accesskit")]
pub(crate) struct PopupIterator<'a>(usize, &'a [(WindowId, PopupDescriptor, Offset)]);
#[cfg(feature = "accesskit")]
impl<'a> Iterator for PopupIterator<'a> {
type Item = &'a PopupDescriptor;
fn next(&mut self) -> Option<Self::Item> {
let i = self.0;
if i < self.1.len() {
self.0 = i + 1;
Some(&self.1[i].1)
} else {
None
}
}
}
#[autoimpl(for<T: trait + ?Sized> Box<T>)]
pub(crate) trait WindowErased: Tile {
fn title(&self) -> &str;
fn properties(&self) -> &Properties;
fn show_tooltip(&mut self, cx: &mut EventCx, id: Id, text: String);
fn close_tooltip(&mut self, cx: &mut EventCx);
#[cfg(feature = "accesskit")]
fn iter_popups(&self) -> PopupIterator<'_>;
}
#[autoimpl(for<T: trait + ?Sized> Box<T>)]
pub(crate) trait WindowWidget: WindowErased + Widget {
fn add_popup(
&mut self,
cx: &mut SizeCx,
data: &Self::Data,
id: WindowId,
popup: PopupDescriptor,
);
fn remove_popup(&mut self, cx: &mut SizeCx, id: WindowId);
fn resize_popups(&mut self, cx: &mut SizeCx, data: &Self::Data);
}
pub(crate) struct Properties {
icon: Option<Icon>, decorations: Decorations,
restrictions: (bool, bool),
drag_anywhere: bool,
transparent: bool,
escapable: bool,
alt_bypass: bool,
disable_nav_focus: bool,
pub(crate) modal_parent: Option<WindowId>,
}
impl Default for Properties {
fn default() -> Self {
Properties {
icon: None,
decorations: Decorations::Server,
restrictions: (true, false),
drag_anywhere: true,
transparent: false,
escapable: false,
alt_bypass: false,
disable_nav_focus: false,
modal_parent: None,
}
}
}
impl Properties {
pub(crate) fn icon(&self) -> Option<Icon> {
self.icon.clone()
}
pub fn decorations(&self) -> Decorations {
self.decorations
}
pub fn restrictions(&self) -> (bool, bool) {
self.restrictions
}
pub fn transparent(&self) -> bool {
self.transparent
}
}
pub struct BoxedWindow<Data: 'static>(pub(crate) Box<dyn WindowWidget<Data = Data>>);
#[impl_self]
mod Window {
#[widget]
pub struct Window<Data: AppData> {
core: widget_core!(),
props: Properties,
#[widget]
inner: Box<dyn Widget<Data = Data>>,
#[widget(&())]
tooltip: Popup<Label<String>>,
#[widget(&())]
title_bar: TitleBar,
#[widget(&())]
b_w: Border,
#[widget(&())]
b_e: Border,
#[widget(&())]
b_n: Border,
#[widget(&())]
b_s: Border,
#[widget(&())]
b_nw: Border,
#[widget(&())]
b_ne: Border,
#[widget(&())]
b_sw: Border,
#[widget(&())]
b_se: Border,
bar_h: i32,
bar_margin: i32,
dec_offset: Offset,
dec_size: Size,
popups: SmallVec<[(WindowId, PopupDescriptor, Offset); 16]>,
}
impl Layout for Self {
fn size_rules(&mut self, cx: &mut SizeCx, mut axis: AxisInfo) -> SizeRules {
let frame_width = self.dec_size.extract(axis.flipped());
axis.map_other(|w| w - frame_width);
let mut inner = self.inner.size_rules(cx, axis);
self.bar_h = 0;
if matches!(self.props.decorations, Decorations::Toolkit) {
let bar = self.title_bar.size_rules(cx, axis);
if axis.is_horizontal() {
inner.max_with(bar);
} else {
self.bar_h = bar.min_size();
self.bar_margin = bar.margins_i32().1.max(inner.margins_i32().0);
inner = bar.appended(inner);
}
}
let _ = self.b_w.size_rules(cx, axis);
let _ = self.b_e.size_rules(cx, axis);
let _ = self.b_n.size_rules(cx, axis);
let _ = self.b_s.size_rules(cx, axis);
let _ = self.b_nw.size_rules(cx, axis);
let _ = self.b_ne.size_rules(cx, axis);
let _ = self.b_se.size_rules(cx, axis);
let _ = self.b_sw.size_rules(cx, axis);
if matches!(
self.props.decorations,
Decorations::Border | Decorations::Toolkit
) {
let frame = cx.frame(FrameStyle::Window, axis);
let (rules, offset, size) = frame.surround(inner);
self.dec_offset.set_component(axis, offset);
self.dec_size.set_component(axis, size);
rules
} else {
inner
}
}
fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
self.core.set_rect(rect);
let s_nw: Size = self.dec_offset.cast();
let s_se = self.dec_size - s_nw;
let mut s_in = rect.size - self.dec_size;
let p_nw = rect.pos;
let mut p_in = p_nw + self.dec_offset;
let p_se = p_in + s_in;
self.b_w.set_rect(
cx,
Rect::new(Coord(p_nw.0, p_in.1), Size(s_nw.0, s_in.1)),
hints,
);
self.b_e.set_rect(
cx,
Rect::new(Coord(p_se.0, p_in.1), Size(s_se.0, s_in.1)),
hints,
);
self.b_n.set_rect(
cx,
Rect::new(Coord(p_in.0, p_nw.1), Size(s_in.0, s_nw.1)),
hints,
);
self.b_s.set_rect(
cx,
Rect::new(Coord(p_in.0, p_se.1), Size(s_in.0, s_se.1)),
hints,
);
self.b_nw.set_rect(cx, Rect::new(p_nw, s_nw), hints);
self.b_ne.set_rect(
cx,
Rect::new(Coord(p_se.0, p_nw.1), Size(s_se.0, s_nw.1)),
hints,
);
self.b_se.set_rect(cx, Rect::new(p_se, s_se), hints);
self.b_sw.set_rect(
cx,
Rect::new(Coord(p_nw.0, p_se.1), Size(s_nw.0, s_se.1)),
hints,
);
if self.bar_h > 0 {
let bar_size = Size(s_in.0, self.bar_h);
self.title_bar
.set_rect(cx, Rect::new(p_in, bar_size), hints);
p_in.1 += self.bar_h + self.bar_margin;
s_in -= Size(0, self.bar_h + self.bar_margin);
}
self.inner.set_rect(cx, Rect::new(p_in, s_in), hints);
}
fn draw(&self, mut draw: DrawCx) {
for (_, popup, translation) in &self.popups {
if let Some(child) = self.find_tile(&popup.id) {
let clip_rect = child.rect() - *translation;
draw.with_overlay(clip_rect, *translation, |draw| {
child.draw(draw);
});
}
}
if self.dec_size != Size::ZERO {
draw.frame(self.rect(), FrameStyle::Window, Default::default());
if self.bar_h > 0 {
self.title_bar.draw(draw.re());
}
}
self.inner.draw(draw.re());
}
}
impl Tile for Self {
fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
Role::Window
}
}
impl Events for Self {
type Data = Data;
fn probe(&self, coord: Coord) -> Id {
for (_, popup, translation) in self.popups.iter().rev() {
if let Some(widget) = self.inner.find_tile(&popup.id)
&& let Some(id) = widget.try_probe(coord + *translation)
{
return id;
}
}
if self.bar_h > 0
&& let Some(id) = self.title_bar.try_probe(coord)
{
return id;
}
self.inner
.try_probe(coord)
.or_else(|| self.b_w.try_probe(coord))
.or_else(|| self.b_e.try_probe(coord))
.or_else(|| self.b_n.try_probe(coord))
.or_else(|| self.b_s.try_probe(coord))
.or_else(|| self.b_nw.try_probe(coord))
.or_else(|| self.b_ne.try_probe(coord))
.or_else(|| self.b_sw.try_probe(coord))
.or_else(|| self.b_se.try_probe(coord))
.unwrap_or_else(|| self.id())
}
fn configure(&mut self, cx: &mut ConfigCx) {
if cx.platform().is_wayland() && self.props.decorations == Decorations::Server {
self.props.decorations = Decorations::Toolkit;
}
if self.props.alt_bypass {
cx.config.alt_bypass = true;
}
if self.props.disable_nav_focus {
cx.config.nav_focus = false;
}
}
fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
match event {
Event::Command(Command::Escape, _) => {
if let Some(id) = self.popups.last().map(|desc| desc.0) {
cx.close_window(id);
} else if self.props.escapable {
cx.close_own_window();
}
Used
}
Event::PressStart(_) if self.props.drag_anywhere => {
cx.drag_window();
Used
}
Event::Timer(handle) if handle == crate::event::Mouse::TIMER_TOOLTIP => {
cx.timer_expiry_tooltip(self);
Used
}
_ => Unused,
}
}
fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
if let Some(kas::messages::SetWindowTitle(title)) = cx.try_pop() {
self.title_bar.set_title(cx, title);
if self.props.decorations == Decorations::Server
&& let Some(w) = cx.winit_window()
{
w.set_title(self.title());
}
} else if let Some(kas::messages::SetWindowIcon(icon)) = cx.try_pop() {
if self.props.decorations == Decorations::Server
&& let Some(w) = cx.winit_window()
{
w.set_window_icon(icon);
return; }
self.props.icon = icon;
}
}
fn handle_scroll(&mut self, cx: &mut EventCx, data: &Data, _: Scroll) {
self.resize_popups(&mut cx.size_cx(), data);
}
}
impl WindowErased for Self {
fn title(&self) -> &str {
self.title_bar.title()
}
fn properties(&self) -> &Properties {
&self.props
}
fn show_tooltip(&mut self, cx: &mut EventCx, id: Id, text: String) {
self.tooltip.inner.set_string(cx, text);
self.tooltip.open(cx, &(), id, false);
}
fn close_tooltip(&mut self, cx: &mut EventCx) {
self.tooltip.close(cx);
}
#[cfg(feature = "accesskit")]
fn iter_popups(&self) -> PopupIterator<'_> {
PopupIterator(0, &self.popups)
}
}
impl std::fmt::Debug for Self {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Window")
.field("core", &self.core)
.field("title", &self.title_bar.title())
.finish()
}
}
}
impl Window<()> {
pub(crate) fn map_any<Data: AppData>(self) -> Window<Data> {
Window {
core: Default::default(),
props: self.props,
inner: Box::new(MapAny::new(self.inner)),
tooltip: self.tooltip,
title_bar: self.title_bar,
b_w: self.b_w,
b_e: self.b_e,
b_n: self.b_n,
b_s: self.b_s,
b_nw: self.b_nw,
b_ne: self.b_ne,
b_sw: self.b_sw,
b_se: self.b_se,
bar_h: 0,
bar_margin: 0,
dec_offset: Default::default(),
dec_size: Default::default(),
popups: self.popups,
}
}
}
impl<Data: AppData> Window<Data> {
pub fn new(ui: impl Widget<Data = Data> + 'static, title: impl ToString) -> Self {
Self::new_boxed(Box::new(ui), title)
}
pub fn new_boxed(ui: Box<dyn Widget<Data = Data>>, title: impl ToString) -> Self {
Window {
core: Default::default(),
props: Properties::default(),
inner: ui,
tooltip: Popup::new(Label::new("".to_string()), Direction::Down).align(Align::Center),
title_bar: TitleBar::new(title),
b_w: Border::new(ResizeDirection::West),
b_e: Border::new(ResizeDirection::East),
b_n: Border::new(ResizeDirection::North),
b_s: Border::new(ResizeDirection::South),
b_nw: Border::new(ResizeDirection::NorthWest),
b_ne: Border::new(ResizeDirection::NorthEast),
b_sw: Border::new(ResizeDirection::SouthWest),
b_se: Border::new(ResizeDirection::SouthEast),
bar_h: 0,
bar_margin: 0,
dec_offset: Default::default(),
dec_size: Default::default(),
popups: Default::default(),
}
}
#[inline]
pub fn boxed(self) -> BoxedWindow<Data> {
BoxedWindow(Box::new(self))
}
pub fn title(&self) -> &str {
self.title_bar.title()
}
pub fn with_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
self.props.icon = icon.into();
self
}
pub fn decorations(&self) -> Decorations {
self.props.decorations
}
pub fn with_decorations(mut self, decorations: Decorations) -> Self {
self.props.decorations = decorations;
self
}
pub fn restrictions(&self) -> (bool, bool) {
self.props.restrictions
}
pub fn with_restrictions(mut self, restrict_min: bool, restrict_max: bool) -> Self {
self.props.restrictions = (restrict_min, restrict_max);
let resizable = !restrict_min || !restrict_max;
self.b_w.set_resizable(resizable);
self.b_e.set_resizable(resizable);
self.b_n.set_resizable(resizable);
self.b_s.set_resizable(resizable);
self.b_nw.set_resizable(resizable);
self.b_ne.set_resizable(resizable);
self.b_se.set_resizable(resizable);
self.b_sw.set_resizable(resizable);
self
}
pub fn drag_anywhere(&self) -> bool {
self.props.drag_anywhere
}
pub fn with_drag_anywhere(mut self, drag_anywhere: bool) -> Self {
self.props.drag_anywhere = drag_anywhere;
self
}
pub fn transparent(&self) -> bool {
self.props.transparent
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.props.transparent = transparent;
self
}
pub fn escapable(mut self) -> Self {
self.props.escapable = true;
self
}
pub fn with_alt_bypass(mut self) -> Self {
self.props.alt_bypass = true;
self
}
pub fn without_nav_focus(mut self) -> Self {
self.props.disable_nav_focus = true;
self
}
pub fn set_modal_with_parent(&mut self, parent: WindowId) {
self.props.modal_parent = Some(parent);
}
}
impl<Data: AppData> WindowWidget for Window<Data> {
fn add_popup(&mut self, cx: &mut SizeCx, data: &Data, id: WindowId, popup: PopupDescriptor) {
let index = 'index: {
for i in 0..self.popups.len() {
if self.popups[i].0 == id {
debug_assert_eq!(self.popups[i].1.id, popup.id);
self.popups[i].1 = popup;
break 'index i;
}
}
let len = self.popups.len();
self.popups.push((id, popup, Offset::ZERO));
len
};
self.resize_popup(cx, data, index);
cx.confirm_popup_is_sized(id);
cx.region_moved();
}
fn remove_popup(&mut self, cx: &mut SizeCx, id: WindowId) {
for i in 0..self.popups.len() {
if id == self.popups[i].0 {
self.popups.remove(i);
cx.region_moved();
return;
}
}
}
fn resize_popups(&mut self, cx: &mut SizeCx, data: &Data) {
for i in 0..self.popups.len() {
self.resize_popup(cx, data, i);
}
}
}
impl<Data: AppData> Window<Data> {
fn resize_popup(&mut self, cx: &mut SizeCx, data: &Data, index: usize) {
let r = self.rect();
let popup = self.popups[index].1.clone();
let is_reversed = popup.direction.is_reversed();
let place_in = |rp, rs: i32, cp: i32, cs: i32, ideal, m: (u16, u16)| -> (i32, i32) {
let m: (i32, i32) = (m.0.into(), m.1.into());
let before: i32 = cp - (rp + m.1);
let before = before.max(0);
let after = (rs - (cs + before + m.0)).max(0);
if after >= ideal {
if is_reversed && before >= ideal {
(cp - ideal - m.1, ideal)
} else {
(cp + cs + m.0, ideal)
}
} else if before >= ideal {
(cp - ideal - m.1, ideal)
} else if before > after {
(rp, before)
} else {
(cp + cs + m.0, after)
}
};
let place_out = |rp, rs, cp: i32, cs, ideal: i32, align| -> (i32, i32) {
let mut size = ideal.max(cs).min(rs);
let pos = match align {
Align::Default | Align::TL => cp,
Align::BR => cp + cs,
Align::Center => cp + (cs - size) / 2,
Align::Stretch => {
size = size.max(cs);
cp
}
};
let pos = pos.min(rp + rs - size).max(rp);
(pos, size)
};
let Some((c, t)) = self.as_tile().find_tile_rect(&popup.parent) else {
return;
};
self.popups[index].2 = t;
let r = r + t; let result = Widget::as_node(self, data).find_node(&popup.id, |mut node| {
let mut cache = layout::SolveCache::default();
cache.find_constraints(node.re(), cx);
let ideal = cache.ideal(false);
let m = cache.margins();
let rect = if popup.direction.is_horizontal() {
let (x, w) = place_in(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0, m.horiz);
let (y, h) = place_out(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1, popup.align);
Rect::new(Coord(x, y), Size::new(w, h))
} else {
let (x, w) = place_out(r.pos.0, r.size.0, c.pos.0, c.size.0, ideal.0, popup.align);
let (y, h) = place_in(r.pos.1, r.size.1, c.pos.1, c.size.1, ideal.1, m.vert);
Rect::new(Coord(x, y), Size::new(w, h))
};
cache.apply_rect(node.re(), cx, rect, false);
cache.print_widget_heirarchy(node.as_tile());
});
assert!(result.is_some());
}
}