use super::{PendingAction, Platform, ProxyAction};
use super::{SharedState, ShellShared, ShellWindow, WindowSurface};
use kas::cast::Cast;
use kas::draw::{color::Rgba, AnimationState, DrawShared};
use kas::event::{config::WindowConfig, ConfigCx, CursorIcon, EventState};
use kas::geom::{Coord, Rect, Size};
use kas::layout::SolveCache;
use kas::theme::{DrawCx, SizeCx, ThemeControl, ThemeSize};
use kas::theme::{Theme, Window as _};
#[cfg(all(wayland_platform, feature = "clipboard"))]
use kas::util::warn_about_error;
use kas::{Action, AppData, ErasedStack, Layout, LayoutExt, Widget, WindowId};
use std::any::TypeId;
use std::mem::take;
use std::time::Instant;
use winit::event::WindowEvent;
use winit::event_loop::EventLoopWindowTarget;
use winit::window::WindowBuilder;
#[crate::autoimpl(Deref, DerefMut using self.window)]
struct WindowData<S: WindowSurface, T: Theme<S::Shared>> {
window: winit::window::Window,
#[cfg(all(wayland_platform, feature = "clipboard"))]
wayland_clipboard: Option<smithay_clipboard::Clipboard>,
surface: S,
window_id: WindowId,
solve_cache: SolveCache,
theme_window: T::Window,
next_avail_frame_time: Instant,
queued_frame_time: Option<Instant>,
}
pub struct Window<A: AppData, S: WindowSurface, T: Theme<S::Shared>> {
_data: std::marker::PhantomData<A>,
widget: kas::Window<A>,
pub(super) window_id: WindowId,
ev_state: EventState,
window: Option<WindowData<S, T>>,
}
impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
pub(super) fn new(
shared: &SharedState<A, S, T>,
window_id: WindowId,
widget: kas::Window<A>,
) -> Self {
let config = WindowConfig::new(shared.config.clone());
Window {
_data: std::marker::PhantomData,
widget,
window_id,
ev_state: EventState::new(config),
window: None,
}
}
pub(super) fn resume(
&mut self,
shared: &mut SharedState<A, S, T>,
elwt: &EventLoopWindowTarget<ProxyAction>,
) -> super::Result<winit::window::WindowId> {
let time = Instant::now();
let use_logical_size = shared.shell.platform.is_wayland();
let scale_factor = if use_logical_size {
1.0
} else {
shared.scale_factor as f32
};
let mut theme_window = shared.shell.theme.new_window(scale_factor);
let dpem = theme_window.size().dpem();
self.ev_state.update_config(scale_factor, dpem);
self.ev_state.full_configure(
theme_window.size(),
&mut shared.shell.draw,
self.window_id,
&mut self.widget,
&shared.data,
);
let node = self.widget.as_node(&shared.data);
let sizer = SizeCx::new(theme_window.size());
let mut solve_cache = SolveCache::find_constraints(node, sizer);
let ideal = solve_cache.ideal(true).max(Size(1, 1));
let ideal = match use_logical_size {
false => ideal.as_physical(),
true => ideal.as_logical(),
};
let mut builder = WindowBuilder::new().with_inner_size(ideal);
let (restrict_min, restrict_max) = self.widget.restrictions();
if restrict_min {
let min = solve_cache.min(true);
let min = match use_logical_size {
false => min.as_physical(),
true => min.as_logical(),
};
builder = builder.with_min_inner_size(min);
}
if restrict_max {
builder = builder.with_max_inner_size(ideal);
}
let window = builder
.with_title(self.widget.title())
.with_window_icon(self.widget.icon())
.with_decorations(self.widget.decorations() == kas::Decorations::Server)
.with_transparent(self.widget.transparent())
.build(elwt)?;
let scale_factor = window.scale_factor();
shared.scale_factor = scale_factor;
let size: Size = window.inner_size().cast();
log::info!(
"new: constructed with physical size {:?}, scale factor {}",
size,
scale_factor
);
if use_logical_size && scale_factor != 1.0 {
let scale_factor = scale_factor as f32;
shared
.shell
.theme
.update_window(&mut theme_window, scale_factor);
let dpem = theme_window.size().dpem();
self.ev_state.update_config(scale_factor, dpem);
solve_cache.invalidate_rule_cache();
}
#[cfg(all(wayland_platform, feature = "clipboard"))]
use winit::window::raw_window_handle::{
HasRawDisplayHandle, RawDisplayHandle, WaylandDisplayHandle,
};
#[cfg(all(wayland_platform, feature = "clipboard"))]
let wayland_clipboard = match window.raw_display_handle() {
RawDisplayHandle::Wayland(WaylandDisplayHandle { display, .. }) => {
Some(unsafe { smithay_clipboard::Clipboard::new(display) })
}
_ => None,
};
let surface = S::new(&mut shared.shell.draw.draw, size, &window)?;
let winit_id = window.id();
self.window = Some(WindowData {
window,
#[cfg(all(wayland_platform, feature = "clipboard"))]
wayland_clipboard,
surface,
window_id: self.window_id,
solve_cache,
theme_window,
next_avail_frame_time: time,
queued_frame_time: Some(time),
});
self.apply_size(shared, true);
log::trace!(target: "kas_perf::wgpu::window", "resume: {}µs", time.elapsed().as_micros());
Ok(winit_id)
}
pub(super) fn suspend(&mut self) {
self.window = None;
}
pub(super) fn handle_event(&mut self, shared: &mut SharedState<A, S, T>, event: WindowEvent) {
let Some(ref mut window) = self.window else {
return;
};
match event {
WindowEvent::Destroyed => (),
WindowEvent::Resized(size) => {
if window
.surface
.do_resize(&mut shared.shell.draw.draw, size.cast())
{
self.apply_size(shared, false);
}
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
shared.scale_factor = scale_factor;
let scale_factor = scale_factor as f32;
shared
.shell
.theme
.update_window(&mut window.theme_window, scale_factor);
let dpem = window.theme_window.size().dpem();
self.ev_state.update_config(scale_factor, dpem);
window.solve_cache.invalidate_rule_cache();
}
event => {
let mut tkw = TkWindow::new(&mut shared.shell, window);
let mut messages = ErasedStack::new();
self.ev_state.with(&mut tkw, &mut messages, |cx| {
cx.handle_winit(&shared.data, &mut self.widget, event);
});
shared.handle_messages(&mut messages);
if self.ev_state.action.contains(Action::RECONFIGURE) {
self.reconfigure(shared);
self.ev_state.action.remove(Action::RECONFIGURE);
}
}
}
}
pub(super) fn flush_pending(
&mut self,
shared: &mut SharedState<A, S, T>,
) -> (Action, Option<Instant>) {
let Some(ref window) = self.window else {
return (Action::empty(), None);
};
let mut tkw = TkWindow::new(&mut shared.shell, window);
let mut messages = ErasedStack::new();
let action =
self.ev_state
.flush_pending(&mut tkw, &mut messages, &mut self.widget, &shared.data);
shared.handle_messages(&mut messages);
if action.contains(Action::CLOSE | Action::EXIT) {
return (action, None);
}
self.handle_action(shared, action);
let mut resume = self.ev_state.next_resume();
let window = self.window.as_mut().unwrap();
if let Some(time) = window.queued_frame_time {
if time <= Instant::now() {
window.request_redraw();
window.queued_frame_time = None;
} else {
resume = resume.map(|t| t.min(time)).or(Some(time));
}
}
(action, resume)
}
pub(super) fn handle_action(&mut self, shared: &mut SharedState<A, S, T>, mut action: Action) {
if action.contains(Action::EVENT_CONFIG) {
if let Some(ref mut window) = self.window {
let scale_factor = window.scale_factor() as f32;
let dpem = window.theme_window.size().dpem();
self.ev_state.update_config(scale_factor, dpem);
action |= Action::UPDATE;
}
}
if action.contains(Action::RECONFIGURE) {
self.reconfigure(shared);
} else if action.contains(Action::UPDATE) {
self.update(shared);
}
if action.contains(Action::THEME_UPDATE) {
if let Some(ref mut window) = self.window {
let scale_factor = window.scale_factor() as f32;
shared
.shell
.theme
.update_window(&mut window.theme_window, scale_factor);
}
}
if action.contains(Action::RESIZE) {
if let Some(ref mut window) = self.window {
window.solve_cache.invalidate_rule_cache();
}
self.apply_size(shared, false);
} else if action.contains(Action::SET_RECT) {
self.apply_size(shared, false);
}
if action.contains(Action::REGION_MOVED) {
self.ev_state.region_moved(&mut self.widget, &shared.data);
}
if !action.is_empty() {
if let Some(ref mut window) = self.window {
window.queued_frame_time = Some(window.next_avail_frame_time);
}
}
}
pub(super) fn update_timer(&mut self, shared: &mut SharedState<A, S, T>) -> Option<Instant> {
let Some(ref window) = self.window else {
return None;
};
let mut tkw = TkWindow::new(&mut shared.shell, window);
let widget = self.widget.as_node(&shared.data);
let mut messages = ErasedStack::new();
self.ev_state
.with(&mut tkw, &mut messages, |cx| cx.update_timer(widget));
shared.handle_messages(&mut messages);
self.next_resume()
}
pub(super) fn add_popup(
&mut self,
shared: &mut SharedState<A, S, T>,
id: WindowId,
popup: kas::PopupDescriptor,
) {
let Some(ref window) = self.window else {
return;
};
let mut tkw = TkWindow::new(&mut shared.shell, window);
let mut messages = ErasedStack::new();
self.ev_state.with(&mut tkw, &mut messages, |cx| {
self.widget.add_popup(cx, &shared.data, id, popup)
});
shared.handle_messages(&mut messages);
}
pub(super) fn send_action(&mut self, action: Action) {
self.ev_state.send_action(action);
}
pub(super) fn send_close(&mut self, shared: &mut SharedState<A, S, T>, id: WindowId) {
if id == self.window_id {
self.ev_state.send_action(Action::CLOSE);
} else if let Some(window) = self.window.as_ref() {
let mut tkw = TkWindow::new(&mut shared.shell, window);
let widget = &mut self.widget;
let mut messages = ErasedStack::new();
self.ev_state
.with(&mut tkw, &mut messages, |cx| widget.remove_popup(cx, id));
shared.handle_messages(&mut messages);
}
}
}
impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
fn reconfigure(&mut self, shared: &mut SharedState<A, S, T>) {
let time = Instant::now();
let Some(ref mut window) = self.window else {
return;
};
self.ev_state.full_configure(
window.theme_window.size(),
&mut shared.shell.draw,
self.window_id,
&mut self.widget,
&shared.data,
);
log::trace!(target: "kas_perf::wgpu::window", "reconfigure: {}µs", time.elapsed().as_micros());
}
fn update(&mut self, shared: &mut SharedState<A, S, T>) {
let time = Instant::now();
let Some(ref mut window) = self.window else {
return;
};
let size = window.theme_window.size();
let mut cx = ConfigCx::new(&size, &mut shared.shell.draw, &mut self.ev_state);
cx.update(self.widget.as_node(&shared.data));
log::trace!(target: "kas_perf::wgpu::window", "update: {}µs", time.elapsed().as_micros());
}
fn apply_size(&mut self, shared: &mut SharedState<A, S, T>, first: bool) {
let time = Instant::now();
let Some(ref mut window) = self.window else {
return;
};
let rect = Rect::new(Coord::ZERO, window.surface.size());
log::debug!("apply_size: rect={rect:?}");
let solve_cache = &mut window.solve_cache;
let mut cx = ConfigCx::new(
window.theme_window.size(),
&mut shared.shell.draw,
&mut self.ev_state,
);
solve_cache.apply_rect(self.widget.as_node(&shared.data), &mut cx, rect, true);
if first {
solve_cache.print_widget_heirarchy(self.widget.as_layout());
}
self.widget.resize_popups(&mut cx, &shared.data);
let (restrict_min, restrict_max) = self.widget.restrictions();
if restrict_min {
let min = window.solve_cache.min(true).as_physical();
window.set_min_inner_size(Some(min));
};
if restrict_max {
let ideal = window.solve_cache.ideal(true).as_physical();
window.set_max_inner_size(Some(ideal));
};
window.request_redraw();
log::trace!(
target: "kas_perf::wgpu::window",
"apply_size: {}µs", time.elapsed().as_micros(),
);
}
pub(super) fn do_draw(&mut self, shared: &mut SharedState<A, S, T>) -> Result<(), ()> {
let start = Instant::now();
let Some(ref mut window) = self.window else {
return Ok(());
};
window.next_avail_frame_time = start + self.ev_state.config().frame_dur();
{
let draw = window.surface.draw_iface(&mut shared.shell.draw);
let mut draw =
shared
.shell
.theme
.draw(draw, &mut self.ev_state, &mut window.theme_window);
let draw_cx = DrawCx::new(&mut draw, self.widget.id());
self.widget.draw(&shared.data, draw_cx);
}
let time2 = Instant::now();
let anim = take(&mut window.surface.common_mut().anim);
window.queued_frame_time = match anim {
AnimationState::None => None,
AnimationState::Animate => Some(window.next_avail_frame_time),
AnimationState::Timed(time) => Some(time.max(window.next_avail_frame_time)),
};
self.ev_state.action -= Action::REDRAW; if !self.ev_state.action.is_empty() {
log::info!("do_draw: abort and enqueue `Self::update` due to non-empty actions");
return Err(());
}
let clear_color = if self.widget.transparent() {
Rgba::TRANSPARENT
} else {
shared.shell.theme.clear_color()
};
window
.surface
.present(&mut shared.shell.draw.draw, clear_color);
let text_dur_micros = take(&mut window.surface.common_mut().dur_text);
let end = Instant::now();
log::trace!(
target: "kas_perf::wgpu::window",
"do_draw: {}µs ({}μs widgets, {}µs text, {}µs render)",
(end - start).as_micros(),
(time2 - start).as_micros(),
text_dur_micros.as_micros(),
(end - time2).as_micros()
);
Ok(())
}
fn next_resume(&self) -> Option<Instant> {
self.window.as_ref().and_then(|w| {
match (self.ev_state.next_resume(), w.queued_frame_time) {
(Some(t1), Some(t2)) => Some(t1.min(t2)),
(Some(t), None) => Some(t),
(None, Some(t)) => Some(t),
(None, None) => None,
}
})
}
}
struct TkWindow<'a, A: AppData, S: WindowSurface, T: Theme<S::Shared>> {
shared: &'a mut ShellShared<A, S::Shared, T>,
window: &'a WindowData<S, T>,
}
impl<'a, A: AppData, S: WindowSurface, T: Theme<S::Shared>> TkWindow<'a, A, S, T> {
fn new(shared: &'a mut ShellShared<A, S::Shared, T>, window: &'a WindowData<S, T>) -> Self {
TkWindow { shared, window }
}
}
impl<'a, A: AppData, S: WindowSurface, T: Theme<S::Shared>> ShellWindow for TkWindow<'a, A, S, T> {
fn add_popup(&mut self, popup: kas::PopupDescriptor) -> WindowId {
let parent_id = self.window.window_id;
let id = self.shared.next_window_id();
self.shared
.pending
.push(PendingAction::AddPopup(parent_id, id, popup));
id
}
unsafe fn add_window(&mut self, window: kas::Window<()>, data_type_id: TypeId) -> WindowId {
if data_type_id != TypeId::of::<A>() {
panic!("add_window: window has wrong Data type!");
}
let window: kas::Window<A> = std::mem::transmute(window);
let id = self.shared.next_window_id();
self.shared
.pending
.push(PendingAction::AddWindow(id, window));
id
}
fn close_window(&mut self, id: WindowId) {
self.shared.pending.push(PendingAction::CloseWindow(id));
}
#[inline]
fn get_clipboard(&mut self) -> Option<String> {
#[cfg(all(wayland_platform, feature = "clipboard"))]
if let Some(cb) = self.window.wayland_clipboard.as_ref() {
return match cb.load() {
Ok(s) => Some(s),
Err(e) => {
warn_about_error("Failed to get clipboard contents", &e);
None
}
};
}
self.shared.get_clipboard()
}
#[inline]
fn set_clipboard<'c>(&mut self, content: String) {
#[cfg(all(wayland_platform, feature = "clipboard"))]
if let Some(cb) = self.window.wayland_clipboard.as_ref() {
cb.store(content);
return;
}
self.shared.set_clipboard(content);
}
#[inline]
fn get_primary(&mut self) -> Option<String> {
#[cfg(all(wayland_platform, feature = "clipboard"))]
if let Some(cb) = self.window.wayland_clipboard.as_ref() {
return match cb.load_primary() {
Ok(s) => Some(s),
Err(e) => {
warn_about_error("Failed to get clipboard contents", &e);
None
}
};
}
self.shared.get_primary()
}
#[inline]
fn set_primary<'c>(&mut self, content: String) {
#[cfg(all(wayland_platform, feature = "clipboard"))]
if let Some(cb) = self.window.wayland_clipboard.as_ref() {
cb.store_primary(content);
return;
}
self.shared.set_primary(content);
}
fn adjust_theme<'s>(&'s mut self, f: Box<dyn FnOnce(&mut dyn ThemeControl) -> Action + 's>) {
let action = f(&mut self.shared.theme);
self.shared.pending.push(PendingAction::Action(action));
}
fn size_and_draw_shared<'s>(
&'s mut self,
f: Box<dyn FnOnce(&dyn ThemeSize, &mut dyn DrawShared) + 's>,
) {
f(self.window.theme_window.size(), &mut self.shared.draw);
}
#[inline]
fn set_cursor_icon(&mut self, icon: CursorIcon) {
self.window.set_cursor_icon(icon);
}
fn platform(&self) -> Platform {
self.shared.platform
}
#[cfg(winit)]
#[inline]
fn winit_window(&self) -> Option<&winit::window::Window> {
Some(self.window)
}
#[inline]
fn waker(&self) -> &std::task::Waker {
&self.shared.waker
}
}