use super::common::{RunError, WindowSurface};
use super::shared::Shared;
use super::{AppData, GraphicsInstance, Platform};
use crate::cast::{Cast, CastApprox};
use crate::config::{Config, WindowConfig};
use crate::draw::PassType;
use crate::draw::color::Rgba;
use crate::event::{ConfigCx, CursorIcon, EventState};
use crate::geom::{Coord, Offset, Rect, Size};
use crate::layout::SolveCache;
use crate::messages::Erased;
use crate::theme::{DrawCx, SizeCx, Theme, ThemeDraw, Window as _};
use crate::window::{BoxedWindow, Decorations, PopupDescriptor, WindowId, WindowWidget};
use crate::{
ActionClose, ActionResize, ConfigAction, Id, Layout, Tile, Widget, WindowActions, autoimpl,
};
#[cfg(windows_platform)]
use raw_window_handle::HasWindowHandle;
use std::cell::RefCell;
use std::mem::take;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use winit::dpi::PhysicalSize;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::{ImeRequest, ImeRequestError, WindowAttributes};
#[crate::autoimpl(Deref, DerefMut using self.window)]
struct WindowData<G: GraphicsInstance> {
window: Arc<Box<dyn winit::window::Window>>,
#[cfg(all(wayland_platform, feature = "clipboard"))]
wayland_clipboard: Option<smithay_clipboard::Clipboard>,
surface: G::Surface,
frame_count: (Instant, u32),
#[cfg(feature = "accesskit")]
accesskit: accesskit_winit::Adapter,
window_id: WindowId,
solve_cache: SolveCache,
need_redraw: bool,
}
#[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
#[cfg_attr(docsrs, doc(cfg(internal_doc)))]
#[autoimpl(Debug ignore self._data, self.widget, self.ev_state, self.theme_and_window)]
pub struct Window<A: AppData, G: GraphicsInstance, T: Theme<G::Shared>> {
_data: std::marker::PhantomData<A>,
pub(super) widget: Box<dyn WindowWidget<Data = A>>,
ev_state: EventState,
theme_and_window: Option<(T::Window, WindowData<G>)>,
}
impl<A: AppData, G: GraphicsInstance, T: Theme<G::Shared>> Window<A, G, T> {
pub fn new(
config: Rc<RefCell<Config>>,
platform: Platform,
window_id: WindowId,
widget: BoxedWindow<A>,
) -> Self {
let config = WindowConfig::new(config);
Window {
_data: std::marker::PhantomData,
widget: widget.0,
ev_state: EventState::new(window_id, config, platform),
theme_and_window: None,
}
}
#[inline]
pub(super) fn window_id(&self) -> WindowId {
self.ev_state.window_id
}
#[inline]
pub(super) fn winit_window(&self) -> Option<&dyn winit::window::Window> {
self.theme_and_window.as_ref().map(|d| &**d.1.window)
}
pub(super) fn create_surfaces(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
el: &dyn ActiveEventLoop,
#[allow(unused)] modal_parent: Option<&dyn winit::window::Window>,
) -> Result<winit::window::WindowId, RunError> {
debug_assert!(self.theme_and_window.is_none());
let time = Instant::now();
let mut max_physical_size = PhysicalSize::new(800, 600);
let mut scale_factor = 1.0;
let mut product = 0;
for monitor in el.available_monitors() {
let Some(size) = monitor.current_video_mode().map(|mode| mode.size()) else {
continue;
};
let p = size.width * size.height;
if p > product {
product = p;
max_physical_size = size;
scale_factor = monitor.scale_factor();
}
}
let max_size = max_physical_size.to_logical::<f64>(scale_factor);
self.ev_state.update_config(scale_factor.cast_approx());
let config = self.ev_state.config();
let mut theme = shared.theme.new_window(config);
let mut node = self.widget.as_node(data);
let _: Option<ActionResize> = self.ev_state.full_configure(theme.size(), node.re());
let mut cx = SizeCx::new(&mut self.ev_state, theme.size());
let mut solve_cache = SolveCache::default();
solve_cache.find_constraints(node, &mut cx);
let min_size = Size(1, 1);
let mut ideal = solve_cache
.ideal(true)
.max(min_size)
.as_physical()
.to_logical::<f64>(scale_factor);
ideal.width = ideal.width.min(max_size.width);
ideal.height = ideal.height.min(max_size.height);
let props = self.widget.properties();
let mut attrs = WindowAttributes::default();
attrs.surface_size = Some(ideal.into());
attrs.title = self.widget.title().to_string();
attrs.visible = false;
let transparent = props.transparent();
attrs.transparent = transparent;
attrs.decorations = props.decorations() == Decorations::Server;
attrs.window_icon = props.icon();
let (restrict_min, restrict_max) = props.restrictions();
if restrict_min {
let mut min = solve_cache
.min(true)
.as_physical()
.to_logical::<f64>(scale_factor);
min.width = min.width.min(max_size.width);
min.height = min.height.min(max_size.height);
attrs.min_surface_size = Some(min.into());
} else {
attrs.min_surface_size = Some(PhysicalSize::new(1, 1).into());
}
if restrict_max {
attrs.max_surface_size = Some(ideal.into());
}
let window = el.create_window(attrs)?;
#[cfg(windows_platform)]
if let Some(_handle) = modal_parent.and_then(|p| p.window_handle().ok()) {
use winit::platform::windows::WindowExtWindows;
window.set_skip_taskbar(true);
}
let new_factor = window.scale_factor();
if new_factor != scale_factor {
scale_factor = new_factor;
self.ev_state.update_config(scale_factor as f32);
let config = self.ev_state.config();
shared.theme.update_window(&mut theme, config);
let mut node = self.widget.as_node(data);
let _: Option<ActionResize> = self.ev_state.full_configure(theme.size(), node.re());
let mut cx = SizeCx::new(&mut self.ev_state, theme.size());
solve_cache.find_constraints(node, &mut cx);
if let Some(mode) = window
.current_monitor()
.and_then(|mon| mon.current_video_mode())
{
max_physical_size = mode.size();
}
let mut ideal = solve_cache.ideal(true).max(min_size).as_physical();
if ideal.width > max_physical_size.width {
ideal.width = max_physical_size.width;
}
if ideal.height > max_physical_size.height {
ideal.height = max_physical_size.height;
}
if let Some(size) = window.request_surface_size(ideal.into()) {
debug_assert_eq!(size, window.surface_size());
} else {
}
}
let size: Size = window.surface_size().cast();
log::info!(
"Window::resume: constructed with physical size {size:?}, scale factor {scale_factor}",
);
#[cfg(all(wayland_platform, feature = "clipboard"))]
use raw_window_handle::{HasDisplayHandle, RawDisplayHandle, WaylandDisplayHandle};
#[cfg(all(wayland_platform, feature = "clipboard"))]
let wayland_clipboard = match window.display_handle() {
Ok(handle) => match handle.as_raw() {
RawDisplayHandle::Wayland(WaylandDisplayHandle { display, .. }) => {
Some(unsafe { smithay_clipboard::Clipboard::new(display.as_ptr()) })
}
_ => None,
},
_ => None,
};
let window = Arc::new(window);
let mut surface = shared.instance.new_surface(window.clone(), transparent)?;
shared.create_draw_shared(&surface)?;
surface.configure(&mut shared.draw.as_mut().unwrap().draw, size);
let winit_id = window.id();
#[cfg(feature = "accesskit")]
let accesskit = todo!();
let window = WindowData {
window,
#[cfg(all(wayland_platform, feature = "clipboard"))]
wayland_clipboard,
surface,
frame_count: (Instant::now(), 0),
#[cfg(feature = "accesskit")]
accesskit,
window_id: self.ev_state.window_id,
solve_cache,
need_redraw: true,
};
self.theme_and_window = Some((theme, window));
self.apply_size(data, true, false);
log::trace!(target: "kas_perf::wgpu::window", "resume: {}µs", time.elapsed().as_micros());
Ok(winit_id)
}
pub(super) fn suspend(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
) -> Option<ActionClose> {
if let Some((ref theme, ref mut window)) = self.theme_and_window {
self.ev_state.suspended(shared);
let actions = self.ev_state.flush_pending(
shared,
theme.size(),
window,
self.widget.as_node(data),
);
actions.close
} else {
None
}
}
pub(super) fn destroy_surfaces(&mut self) {
self.theme_and_window = None;
}
pub(super) fn handle_event(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
event: WindowEvent,
) -> bool {
let Some((ref mut theme, ref mut window)) = self.theme_and_window else {
return false;
};
#[cfg(feature = "accesskit")]
todo!();
let (apply_size, resize, poll) = match event {
WindowEvent::Moved(_) | WindowEvent::Destroyed => return false,
WindowEvent::SurfaceResized(size) => {
if window
.surface
.configure(&mut shared.draw.as_mut().unwrap().draw, size.cast())
{
self.apply_size(data, false, false);
}
(true, false, false)
}
WindowEvent::ScaleFactorChanged {
scale_factor,
mut surface_size_writer,
} => {
if scale_factor as f32 == self.ev_state.config.scale_factor() {
return false;
}
self.ev_state.update_config(scale_factor as f32);
let config = self.ev_state.config();
shared.theme.update_window(theme, config);
self.reconfigure(data);
let Some((ref theme, ref mut window)) = self.theme_and_window else {
unreachable!()
};
let mut cx = SizeCx::new(&mut self.ev_state, theme.size());
window
.solve_cache
.find_constraints(self.widget.as_node(data), &mut cx);
let min = window.solve_cache.min(true);
let size = window.surface.size();
let (restrict_min, _) = self.widget.properties().restrictions();
let apply = if !restrict_min || size >= min {
true
} else {
let size = size.max(min);
surface_size_writer
.request_surface_size(size.as_physical())
.is_err()
};
(apply, false, false)
}
WindowEvent::RedrawRequested => return self.do_draw(shared, data).is_err(),
event => {
let resize = self
.ev_state
.with(shared, theme.size(), window, |cx| {
cx.handle_winit(&mut self.widget, data, event);
})
.is_some();
(resize, resize, false)
}
};
if apply_size {
self.apply_size(data, false, resize);
}
poll
}
pub(super) fn flush_pending(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
) -> (WindowActions, Option<Instant>) {
let Some((ref theme, ref mut window)) = self.theme_and_window else {
return (WindowActions::default(), None);
};
let actions =
self.ev_state
.flush_pending(shared, theme.size(), window, self.widget.as_node(data));
if actions.resize.is_some() {
self.apply_size(data, false, true);
}
if actions.close.is_some() {
return (actions, None);
}
let Some((_, ref mut window)) = self.theme_and_window else {
unreachable!();
};
if actions != WindowActions::default() {
window.need_redraw = true;
}
let resume = match (
self.ev_state.next_resume(),
window.surface.common_mut().next_resume(),
) {
(None, None) => None,
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(Some(a), Some(b)) => Some(a.min(b)),
};
if window.need_redraw || self.ev_state.need_frame_update() {
window.request_redraw();
}
(actions, resume)
}
pub(super) fn send_erased(&mut self, id: Id, msg: Erased) {
self.ev_state.send_erased(id, msg);
}
pub(super) fn config_update(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
action: ConfigAction,
) {
let Some((ref mut theme, ref mut window)) = self.theme_and_window else {
return;
};
if action.contains(ConfigAction::EVENT) {
self.ev_state.update_config(window.scale_factor() as f32);
}
let resize = if action.contains(ConfigAction::THEME_SWITCH) {
let config = self.ev_state.config();
*theme = shared.theme.new_window(config);
true
} else if action.contains(ConfigAction::THEME) {
let config = self.ev_state.config();
shared.theme.update_window(theme, config)
} else {
false
};
self.reconfigure(data);
if resize {
self.apply_size(data, false, true);
}
}
#[cfg(feature = "accesskit")]
pub(super) fn accesskit_event(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
event: accesskit_winit::WindowEvent,
) {
let Some((ref theme, ref mut window)) = self.theme_and_window else {
return;
};
use accesskit_winit::WindowEvent as WE;
match event {
WE::InitialTreeRequested => window
.accesskit
.update_if_active(|| self.ev_state.accesskit_tree_update(&self.widget)),
WE::ActionRequested(request) => {
let resize = self.ev_state.with(shared, theme.size(), window, |cx| {
cx.handle_accesskit_action(self.widget.as_node(data), request);
});
if resize.is_some() {
self.apply_size(data, false, true);
}
}
WE::AccessibilityDeactivated => {
self.ev_state.disable_accesskit();
}
}
}
pub(super) fn update_timer(
&mut self,
shared: &mut Shared<A, G, T>,
data: &A,
requested_resume: Instant,
) {
let Some((ref theme, ref mut window)) = self.theme_and_window else {
return;
};
if window.surface.common_mut().immediate_redraw() {
window.need_redraw = true;
window.request_redraw();
}
let widget = self.widget.as_node(data);
if Some(requested_resume) == self.ev_state.next_resume() {
let resize = self.ev_state.with(shared, theme.size(), window, |cx| {
cx.update_timer(widget);
});
if resize.is_some() {
self.apply_size(data, false, true);
}
} else {
#[allow(clippy::drop_non_drop)]
drop(widget); }
}
pub(super) fn add_popup(&mut self, data: &A, id: WindowId, popup: PopupDescriptor) {
let Some((ref theme, _)) = self.theme_and_window else {
return;
};
let mut cx = SizeCx::new(&mut self.ev_state, theme.size());
self.widget.add_popup(&mut cx, data, id, popup);
}
pub(super) fn send_close(&mut self, id: WindowId) {
if id == self.ev_state.window_id {
self.ev_state.close_own_window();
} else if let Some((ref theme, _)) = self.theme_and_window {
let widget = &mut self.widget;
let mut cx = SizeCx::new(&mut self.ev_state, theme.size());
widget.remove_popup(&mut cx, id);
}
}
}
impl<A: AppData, G: GraphicsInstance, T: Theme<G::Shared>> Window<A, G, T> {
fn reconfigure(&mut self, data: &A) {
let time = Instant::now();
let Some((ref theme, _)) = self.theme_and_window else {
return;
};
let resize: Option<ActionResize> = self
.ev_state
.full_configure(theme.size(), self.widget.as_node(data));
if resize.is_some() {
self.apply_size(data, false, true);
}
log::trace!(target: "kas_perf::wgpu::window", "reconfigure: {}µs", time.elapsed().as_micros());
}
pub(super) fn update(&mut self, data: &A) {
let time = Instant::now();
let Some((ref theme, ref mut window)) = self.theme_and_window else {
return;
};
let size = theme.size();
let mut cx = ConfigCx::new(&size, &mut self.ev_state);
cx.update(self.widget.as_node(data));
if cx.needs_resize() {
self.apply_size(data, false, true);
} else if cx.needs_redraw() {
window.request_redraw();
}
log::trace!(target: "kas_perf::wgpu::window", "update: {}µs", time.elapsed().as_micros());
}
fn apply_size(&mut self, data: &A, first: bool, resize: bool) {
let time = Instant::now();
let Some((ref theme, ref mut window)) = self.theme_and_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 = SizeCx::new(&mut self.ev_state, theme.size());
if resize {
solve_cache.find_constraints(self.widget.as_node(data), &mut cx);
}
solve_cache.apply_rect(self.widget.as_node(data), &mut cx, rect, true);
if first {
solve_cache.print_widget_heirarchy(self.widget.as_tile());
}
debug_assert!(solve_cache.min(false) <= solve_cache.ideal(false));
self.widget.resize_popups(&mut cx, data);
let (restrict_min, restrict_max) = self.widget.properties().restrictions();
let min_size = if restrict_min {
window.solve_cache.min(true).as_physical()
} else {
PhysicalSize::new(1, 1)
};
window.set_min_surface_size(Some(min_size.into()));
window.set_max_surface_size(
restrict_max.then(|| window.solve_cache.ideal(true).as_physical().into()),
);
window.set_visible(true);
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 Shared<A, G, T>, data: &A) -> Result<(), ()> {
let start = Instant::now();
let Some((ref theme, ref window)) = self.theme_and_window else {
return Ok(());
};
let widget = self.widget.as_node(data);
let resize = self.ev_state.with(shared, theme.size(), window, |cx| {
cx.frame_update(widget);
});
if resize.is_some() {
self.apply_size(data, false, true);
}
let Some((ref mut theme, ref mut window)) = self.theme_and_window else {
unreachable!();
};
#[cfg(feature = "accesskit")]
if self.ev_state.accesskit_is_enabled() {
window
.accesskit
.update_if_active(|| self.ev_state.accesskit_tree_update(&self.widget))
}
self.ev_state.clear_access_key_bindings();
{
let rect = Rect::new(Coord::ZERO, window.surface.size());
let draw = window.surface.draw_iface(shared.draw.as_mut().unwrap());
let mut draw = shared.theme.draw(draw, &mut self.ev_state, theme);
let draw_cx = DrawCx::new(&mut draw, self.widget.id());
self.widget.draw(draw_cx);
draw.new_pass(
rect,
Offset::ZERO,
PassType::Clip,
Box::new(|draw: &mut dyn ThemeDraw| draw.event_state_overlay()),
);
}
let time2 = Instant::now();
window.need_redraw = window.surface.common_mut().immediate_redraw();
self.ev_state.action_redraw = None;
let clear_color = if self.widget.properties().transparent() {
Rgba::TRANSPARENT
} else {
shared.theme.clear_color()
};
let time3 = window
.surface
.present(&mut shared.draw.as_mut().unwrap().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, {}μs present)",
(end - start).as_micros(),
(time2 - start).as_micros(),
text_dur_micros.as_micros(),
(time3 - time2).as_micros(),
(end - time2).as_micros()
);
const SECOND: Duration = Duration::from_secs(1);
window.frame_count.1 += 1;
if window.frame_count.0 + SECOND <= end {
log::debug!(
"Window {:?}: {} frames in last second",
window.window_id,
window.frame_count.1
);
window.frame_count.0 = end;
window.frame_count.1 = 0;
}
Ok(())
}
}
pub(crate) trait WindowDataErased {
fn window_id(&self) -> WindowId;
#[cfg(all(wayland_platform, feature = "clipboard"))]
fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard>;
fn set_pointer_icon(&self, icon: CursorIcon);
fn ime_request(&self, request: ImeRequest) -> Result<(), ImeRequestError>;
fn winit_window(&self) -> Option<&dyn winit::window::Window>;
}
impl<G: GraphicsInstance> WindowDataErased for WindowData<G> {
fn window_id(&self) -> WindowId {
self.window_id
}
#[cfg(all(wayland_platform, feature = "clipboard"))]
fn wayland_clipboard(&self) -> Option<&smithay_clipboard::Clipboard> {
self.wayland_clipboard.as_ref()
}
#[inline]
fn set_pointer_icon(&self, icon: CursorIcon) {
self.window.set_cursor(icon.into());
}
fn ime_request(&self, request: ImeRequest) -> Result<(), ImeRequestError> {
self.window.request_ime_update(request)
}
#[inline]
fn winit_window(&self) -> Option<&dyn winit::window::Window> {
Some(&**self.window)
}
}