//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop //! window. use std::cell::RefCell; use std::collections::hash_map; use std::ffi::OsStr; use std::hash::Hash; use std::io; use std::marker::PhantomData; use std::num::{NonZeroU32, TryFromIntError}; use std::ops::{Deref, DerefMut, Not}; use std::path::{Path, PathBuf}; use std::string::ToString; use std::sync::{mpsc, Arc, OnceLock}; use std::time::{Duration, Instant}; use ahash::AHashMap; use alot::LotId; use arboard::Clipboard; use figures::units::{Px, UPx}; use figures::{ FloatConversion, Fraction, IntoSigned, IntoUnsigned, Point, Ranged, Rect, Round, ScreenScale, Size, UPx2D, Zero, }; use image::{DynamicImage, RgbImage, RgbaImage}; use intentional::{Assert, Cast}; use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ ElementState, Ime, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::keyboard::{ Key, KeyLocation, ModifiersState, NamedKey, NativeKeyCode, PhysicalKey, SmolStr, }; use kludgine::app::winit::window::{self, Cursor, Fullscreen, Icon, WindowButtons, WindowLevel}; use kludgine::app::{winit, WindowAttributes, WindowBehavior as _}; use kludgine::cosmic_text::{fontdb, Family, FamilyOwned}; use kludgine::drawing::Drawing; use kludgine::shapes::Shape; use kludgine::wgpu::{self, CompositeAlphaMode, COPY_BYTES_PER_ROW_ALIGNMENT}; use kludgine::{Color, DrawableExt, Kludgine, KludgineId, Origin, Texture}; use parking_lot::{Mutex, MutexGuard}; use sealed::{Ize, PreShowCallback, WindowExecute}; use tracing::Level; use unicode_segmentation::UnicodeSegmentation; use crate::animation::{ AnimationTarget, Easing, LinearInterpolate, PercentBetween, Spawn, ZeroToOne, }; use crate::app::{Application, Cushy, Open, PendingApp, Run}; use crate::context::sealed::{InvalidationStatus, Trackable as _}; use crate::context::{ AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, Trackable, WidgetContext, }; use crate::fonts::FontCollection; use crate::graphics::{FontState, Graphics}; use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::tree::Tree; use crate::utils::ModifiersExt; use crate::value::{ Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Tracked, Value, }; use crate::widget::{ Callback, EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, SharedCallback, WidgetId, WidgetInstance, HANDLED, IGNORED, }; use crate::widgets::shortcuts::{ShortcutKey, ShortcutMap}; use crate::window::sealed::WindowCommand; use crate::{App, ConstraintLimit}; /// A platform-dependent window implementation. pub trait PlatformWindowImplementation { /// Marks the window to close as soon as possible. fn close(&mut self); /// Returns the underlying `winit` window, if one exists. fn winit(&self) -> Option<&Arc>; /// Sets the window to redraw as soon as possible. fn set_needs_redraw(&mut self); /// Sets the window to redraw after a `duration`. fn redraw_in(&mut self, duration: Duration); /// Sets the window to redraw at a specified instant. fn redraw_at(&mut self, moment: Instant); /// Returns the current keyboard modifiers. fn modifiers(&self) -> Modifiers; /// Returns the amount of time that has elapsed since the last redraw. fn elapsed(&self) -> Duration; /// Sets the current cursor icon to `cursor`. fn set_cursor(&mut self, cursor: Cursor); /// Returns a handle for the window. fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle; /// Returns the current outer position of the window. fn outer_position(&self) -> Point { self.winit().map_or_else(Point::default, |w| { w.outer_position().unwrap_or_default().into() }) } /// Returns the current inner position of the window. fn inner_position(&self) -> Point { self.winit().map_or_else(Point::default, |w| { w.inner_position().unwrap_or_default().into() }) } /// Returns the current inner size of the window. fn inner_size(&self) -> Size; /// Returns the current outer size of the window. fn outer_size(&self) -> Size { self.winit() .map_or_else(|| self.inner_size(), |w| w.outer_size().into()) } /// Returns true if the window can have its size changed. /// /// The provided implementation returns /// [`winit::window::Window::is_resizable`], or true if this window has no /// winit window. fn is_resizable(&self) -> bool { self.winit().map_or(true, |win| win.is_resizable()) } /// Returns true if the window can have its size changed. /// /// The provided implementation returns [`winit::window::Window::theme`], or /// dark if this window has no winit window. fn theme(&self) -> winit::window::Theme { self.winit() .and_then(|win| win.theme()) .unwrap_or(winit::window::Theme::Dark) } /// Requests that the window change its inner size. /// /// The provided implementation forwards the request onto the winit window, /// if present. /// /// The result is the same [`winit::window::Window::request_inner_size`] -- /// if a size is returned, the change was made before the call returns, and /// no resized event will be emitted. #[must_use] fn request_inner_size(&mut self, inner_size: Size) -> Option> { self.winit() .and_then(|winit| winit.request_inner_size(PhysicalSize::from(inner_size))) .map(Size::from) } /// Sets whether [`Ime`] events should be enabled. /// /// The provided implementation forwards the request onto the winit window, /// if present. fn set_ime_allowed(&self, allowed: bool) { if let Some(winit) = self.winit() { winit.set_ime_allowed(allowed); } } /// Sets the location of the cursor. fn set_ime_location(&self, location: Rect) { if let Some(winit) = self.winit() { winit.set_ime_cursor_area( PhysicalPosition::from(location.origin), PhysicalSize::from(location.size), ); } } /// Sets the current [`Ime`] purpose. /// /// The provided implementation forwards the request onto the winit window, /// if present. fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) { if let Some(winit) = self.winit() { winit.set_ime_purpose(purpose); } } /// Sets the window's minimum inner size. fn set_min_inner_size(&self, min_size: Option>) { if let Some(winit) = self.winit() { winit.set_min_inner_size::>(min_size.map(Into::into)); } } /// Sets the window's maximum inner size. fn set_max_inner_size(&self, max_size: Option>) { if let Some(winit) = self.winit() { winit.set_max_inner_size::>(max_size.map(Into::into)); } } /// Ensures that this window will be redrawn when `value` has been updated. fn redraw_when_changed(&self, value: &impl Trackable, invalidation_status: &InvalidationStatus) where Self: Sized, { value.inner_redraw_when_changed(self.handle(invalidation_status.clone())); } } impl PlatformWindowImplementation for kludgine::app::Window<'_, WindowCommand> { fn set_cursor(&mut self, cursor: Cursor) { self.winit().set_cursor(cursor); } fn inner_size(&self) -> Size { self.winit().inner_size().into() } fn close(&mut self) { self.close(); } fn winit(&self) -> Option<&Arc> { Some(self.winit()) } fn set_needs_redraw(&mut self) { self.set_needs_redraw(); } fn redraw_in(&mut self, duration: Duration) { self.redraw_in(duration); } fn redraw_at(&mut self, moment: Instant) { self.redraw_at(moment); } fn modifiers(&self) -> Modifiers { self.modifiers() } fn elapsed(&self) -> Duration { self.elapsed() } fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle { WindowHandle::new(self.handle(), redraw_status) } fn request_inner_size(&mut self, inner_size: Size) -> Option> { self.request_inner_size(inner_size) } } /// A platform-dependent window. pub trait PlatformWindow { /// Marks the window to close as soon as possible. fn close(&mut self); /// Returns a handle for the window. fn handle(&self) -> WindowHandle; /// Returns the unique id of the [`Kludgine`] instance used by this window. fn kludgine_id(&self) -> KludgineId; /// Returns the dynamic that is synchrnoized with the window's focus. fn focused(&self) -> &Dynamic; /// Returns the dynamic that is synchronized with the window's occlusion /// status. fn occluded(&self) -> &Dynamic; /// Returns the current inner size of the window. fn inner_size(&self) -> &Dynamic>; /// Returns the current outer size of the window. fn outer_size(&self) -> Size; /// Returns the shared application resources. fn cushy(&self) -> &Cushy; /// Returns the app managing this window's event loop. fn app(&self) -> Option<&App>; /// Sets the window to redraw as soon as possible. fn set_needs_redraw(&mut self); /// Sets the window to redraw after a `duration`. fn redraw_in(&mut self, duration: Duration); /// Sets the window to redraw at a specified instant. fn redraw_at(&mut self, moment: Instant); /// Returns the current keyboard modifiers. fn modifiers(&self) -> Modifiers; /// Returns the amount of time that has elapsed since the last redraw. fn elapsed(&self) -> Duration; /// Sets the current cursor icon to `cursor`. fn set_cursor(&mut self, cursor: Cursor); /// Sets the location of the cursor. fn set_ime_location(&self, location: Rect); /// Sets whether [`Ime`] events should be enabled. fn set_ime_allowed(&self, allowed: bool); /// Sets the current [`Ime`] purpose. fn set_ime_purpose(&self, purpose: winit::window::ImePurpose); /// Requests that the window change its inner size. #[must_use] fn request_inner_size(&mut self, inner_size: Size) -> Option>; /// Sets the window's minimum inner size. fn set_min_inner_size(&self, min_size: Option>); /// Sets the window's maximum inner size. fn set_max_inner_size(&self, max_size: Option>); /// Returns a handle to the underlying winit window, if available. fn winit(&self) -> Option<&Arc>; } /// A currently running Cushy window. pub struct RunningWindow { window: W, kludgine_id: KludgineId, invalidation_status: InvalidationStatus, app: App, focused: Dynamic, occluded: Dynamic, inner_size: Dynamic>, close_requested: Option>, } impl RunningWindow where W: PlatformWindowImplementation, { #[allow(clippy::too_many_arguments)] pub(crate) fn new( window: W, kludgine_id: KludgineId, invalidation_status: &InvalidationStatus, app: &App, focused: &Dynamic, occluded: &Dynamic, inner_size: &Dynamic>, close_requested: &Option>, ) -> Self { Self { window, kludgine_id, invalidation_status: invalidation_status.clone(), app: app.clone(), focused: focused.clone(), occluded: occluded.clone(), inner_size: inner_size.clone(), close_requested: close_requested.clone(), } } /// Returns the [`KludgineId`] of this window. /// /// Each window has its own unique `KludgineId`. #[must_use] pub const fn kludgine_id(&self) -> KludgineId { self.kludgine_id } /// Returns a dynamic that is updated whenever this window's focus status /// changes. #[must_use] pub const fn focused(&self) -> &Dynamic { &self.focused } /// Returns a dynamic that is updated whenever this window's occlusion /// status changes. #[must_use] pub const fn occluded(&self) -> &Dynamic { &self.occluded } /// Request that the window closes. /// /// A window may disallow itself from being closed by customizing /// [`WindowBehavior::close_requested`]. pub fn request_close(&self) { self.handle().request_close(); } /// Returns a handle to this window. #[must_use] pub fn handle(&self) -> WindowHandle { self.window.handle(self.invalidation_status.clone()) } /// Returns a dynamic that is synchronized with this window's inner size. /// /// Whenever the window is resized, this dynamic will be updated with the /// new inner size. Setting a new value will request the new size from the /// operating system, but resize requests may be altered or ignored by the /// operating system. #[must_use] pub const fn inner_size(&self) -> &Dynamic> { &self.inner_size } /// Returns a locked mutex guard to the OS's clipboard, if one was able to be /// initialized when the window opened. #[must_use] pub fn clipboard_guard(&self) -> Option> { self.app.cushy().clipboard_guard() } } impl Deref for RunningWindow where W: PlatformWindowImplementation + 'static, { type Target = dyn PlatformWindowImplementation; fn deref(&self) -> &Self::Target { &self.window } } impl DerefMut for RunningWindow where W: PlatformWindowImplementation + 'static, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.window } } impl PlatformWindow for RunningWindow where W: PlatformWindowImplementation, { fn close(&mut self) { self.window.close(); } fn handle(&self) -> WindowHandle { self.window.handle(self.invalidation_status.clone()) } fn kludgine_id(&self) -> KludgineId { self.kludgine_id } fn app(&self) -> Option<&App> { Some(&self.app) } fn focused(&self) -> &Dynamic { &self.focused } fn occluded(&self) -> &Dynamic { &self.occluded } fn inner_size(&self) -> &Dynamic> { &self.inner_size } fn outer_size(&self) -> Size { self.window.outer_size() } fn cushy(&self) -> &Cushy { self.app.cushy() } fn set_needs_redraw(&mut self) { self.window.set_needs_redraw(); } fn redraw_in(&mut self, duration: Duration) { self.window.redraw_in(duration); } fn redraw_at(&mut self, moment: Instant) { self.window.redraw_at(moment); } fn modifiers(&self) -> Modifiers { self.window.modifiers() } fn elapsed(&self) -> Duration { self.window.elapsed() } fn set_ime_allowed(&self, allowed: bool) { self.window.set_ime_allowed(allowed); } fn set_ime_purpose(&self, purpose: winit::window::ImePurpose) { self.window.set_ime_purpose(purpose); } fn set_cursor(&mut self, cursor: Cursor) { self.window.set_cursor(cursor); } fn set_min_inner_size(&self, min_size: Option>) { self.window.set_min_inner_size(min_size); } fn set_max_inner_size(&self, max_size: Option>) { self.window.set_max_inner_size(max_size); } fn request_inner_size(&mut self, inner_size: Size) -> Option> { self.window.request_inner_size(inner_size) } fn set_ime_location(&self, location: Rect) { self.window.set_ime_location(location); } fn winit(&self) -> Option<&Arc> { self.window.winit() } } /// A Cushy window that is not yet running. #[must_use] pub struct Window where Behavior: WindowBehavior, { /// The title to display in the title bar of the window. pub title: Value, /// The colors to use to theme the user interface. pub theme: Value, /// When true, the system fonts will be loaded into the font database. This /// is on by default. pub load_system_fonts: bool, /// The list of font families to try to find when a [`FamilyOwned::Serif`] /// font is requested. pub serif_font_family: FontFamilyList, /// The list of font families to try to find when a /// [`FamilyOwned::SansSerif`] font is requested. pub sans_serif_font_family: FontFamilyList, /// The list of font families to try to find when a [`FamilyOwned::Fantasy`] /// font is requested. pub fantasy_font_family: FontFamilyList, /// The list of font families to try to find when a /// [`FamilyOwned::Monospace`] font is requested. pub monospace_font_family: FontFamilyList, /// The list of font families to try to find when a [`FamilyOwned::Cursive`] /// font is requested. pub cursive_font_family: FontFamilyList, /// A collection of fonts that this window will load. pub fonts: FontCollection, /// When true, Cushy will try to use "vertical sync" to try to eliminate /// graphical tearing that can occur if the graphics card has a new frame /// presented while the monitor is currently rendering another frame. /// /// Under the hood, Cushy uses `wgpu::PresentMode::AutoVsync` when true and /// `wgpu::PresentMode::AutoNoVsync` when false. pub vsync: bool, /// The number of samples to perform for each pixel rendered to the screen. /// When 1, multisampling is disabled. pub multisample_count: NonZeroU32, /// Resizes the window to fit the contents if true. pub resize_to_fit: Value, context: Behavior::Context, pending: PendingWindow, attributes: WindowAttributes, on_closed: Option, on_init: Option, on_open: Option>, inner_size: Option>>, zoom: Option>, occluded: Option>, focused: Option>, theme_mode: Option>, content_protected: Option>, cursor_hittest: Option>, cursor_visible: Option>, cursor_position: Option>>, window_level: Option>, decorated: Option>, maximized: Option>, minimized: Option>, resizable: Option>, resize_increments: Option>>, visible: Option>, outer_size: Option>>, inner_position: Option>>, outer_position: Option>>, close_requested: Option>, icon: Option>>, modifiers: Option>, enabled_buttons: Option>, fullscreen: Option>>, shortcuts: Value, on_file_drop: Option>, } impl Default for Window where Behavior: WindowBehavior, Behavior::Context: Default, { fn default() -> Self { Self::new(Behavior::Context::default()) } } impl Window { /// Returns a new instance using `widget` as its contents. pub fn for_widget(widget: W) -> Self where W: MakeWidget, { Self::new(widget.make_widget()) } } impl Window where Behavior: WindowBehavior, { /// Returns a new instance using `context` to initialize the window upon /// opening. pub fn new(context: Behavior::Context) -> Self { Self::new_with_pending(context, PendingWindow::default()) } fn new_with_pending(context: Behavior::Context, pending: PendingWindow) -> Self { static EXECUTABLE_NAME: OnceLock = OnceLock::new(); let title = EXECUTABLE_NAME .get_or_init(|| { std::env::args_os() .next() .and_then(|path| { Path::new(&path) .file_name() .and_then(OsStr::to_str) .map(ToString::to_string) }) .unwrap_or_else(|| String::from("Cushy App")) }) .clone(); Self { pending, title: Value::Constant(title), attributes: WindowAttributes::default(), on_open: None, on_closed: None, context, load_system_fonts: true, theme: Value::default(), occluded: None, focused: None, theme_mode: None, inner_size: None, serif_font_family: FontFamilyList::default(), sans_serif_font_family: FontFamilyList::default(), fantasy_font_family: FontFamilyList::default(), monospace_font_family: FontFamilyList::default(), cursive_font_family: FontFamilyList::default(), fonts: { let fonts = FontCollection::default(); #[cfg(feature = "roboto-flex")] fonts.push(include_bytes!("../assets/RobotoFlex.ttf").to_vec()); fonts }, multisample_count: NonZeroU32::new(4).assert("not 0"), vsync: true, close_requested: None, zoom: None, resize_to_fit: Value::Constant(false), content_protected: None, cursor_hittest: None, cursor_visible: None, cursor_position: None, window_level: None, decorated: None, maximized: None, minimized: None, resizable: None, resize_increments: None, visible: None, outer_size: None, inner_position: None, outer_position: None, icon: None, modifiers: None, enabled_buttons: None, fullscreen: None, shortcuts: Value::default(), on_init: None, on_file_drop: None, } } /// Returns the handle to this window. pub const fn handle(&self) -> &WindowHandle { &self.pending.0 } fn center_on_open(&mut self, app: App) { // We want to ensure that if the user has customized any of these // properties that we keep their dynamic. let outer_position = self.outer_position.clone().unwrap_or_else(|| { let outer_position = Dynamic::new(Point::default()); self.outer_position = Some(outer_position.clone()); outer_position }); let outer_size = self.outer_size.clone().unwrap_or_else(|| { let outer_size = Dynamic::new(Size::default()); self.outer_size = Some(outer_size.clone()); outer_size }); let visible = self.visible.clone().unwrap_or_else(|| { let visible = Dynamic::new(false); self.visible = Some(visible.clone()); visible }); visible.set(false); let callback_handle = Dynamic::new(None); callback_handle.set(Some(outer_size.for_each_subsequent({ let visible = visible.clone(); let callback_handle = callback_handle.clone(); move |new_size| { if let Some(monitor) = app.monitors().and_then(|monitors| { let initial_position = outer_position.get(); monitors .available .into_iter() .find(|m| m.region().contains(initial_position)) .or(monitors.primary) }) { let region = monitor.region(); let margin = region.size - new_size.into_signed(); outer_position.set(region.origin + margin / 2); } visible.set(true); // Uninstall this callback to ensure it doesn't fire again. let _ = callback_handle.take(); } }))); } /// Opens `self` in the center of the monitor the window initially appears /// on. pub fn open_centered(mut self, app: &mut App) -> crate::Result where App: Application + ?Sized, { self.center_on_open(app.as_app()); self.open(app) } /// Sets `focused` to be the dynamic updated when this window's focus status /// is changed. /// /// When the window is focused for user input, the dynamic will contain /// `true`. /// /// The current value of `focused` will inform the OS whether the window /// should be activated upon opening. pub fn focused(mut self, focused: impl IntoValue) -> Self { let focused = focused.into_value(); self.attributes.active = focused.get(); if let Value::Dynamic(focused) = focused { self.focused = Some(focused); } self } /// Sets `occluded` to be the dynamic updated when this window's occlusion /// status is changed. /// /// When the window is occluded (completely hidden/offscreen/minimized), the /// dynamic will contain `true`. If the window is at least partially /// visible, this value will contain `true`. pub fn occluded(mut self, occluded: impl IntoDynamic) -> Self { let occluded = occluded.into_dynamic(); self.occluded = Some(occluded); self } /// Sets the full screen mode for this window. pub fn fullscreen(mut self, fullscreen: impl IntoValue>) -> Self { let fullscreen = fullscreen.into_value(); self.attributes.fullscreen = fullscreen.get(); self.fullscreen = Some(fullscreen); self } /// Sets `inner_size` to be the dynamic synchronized with this window's /// inner size. /// /// When the window is resized, the dynamic will contain its new size. When /// the dynamic is updated with a new value, a resize request will be made /// with the new inner size. pub fn inner_size(mut self, inner_size: impl IntoDynamic>) -> Self { let inner_size = inner_size.into_dynamic(); let initial_size = inner_size.get(); if initial_size.width > 0 && initial_size.height > 0 { self.attributes.inner_size = Some(winit::dpi::Size::Physical(initial_size.into())); } self.inner_size = Some(inner_size); self } /// Sets `outer_size` to be a dynamic synchronized with this window's size, /// including decorations. /// /// When the window is resized, the dynamic will contain its new size. /// Setting this dynamic with a new value does not change the window in any /// way. To resize the window, use [`inner_size`](Self::inner_size). pub fn outer_size(mut self, outer_size: impl IntoDynamic>) -> Self { self.outer_size = Some(outer_size.into_dynamic()); self } /// Sets `position` to be a dynamic synchronized with this window's outer /// position. /// /// If `automatic_layout` is true, the initial value of `position` will be /// ignored and the window server will control the window's initial /// position. /// /// When the window is moved, this dynamic will contain its new position. /// Setting this dynamic will attempt to move the window to the provided /// location. pub fn outer_position( mut self, position: impl IntoValue>, automatic_layout: bool, ) -> Self { let position = position.into_value(); if let Some(initial_position) = (!automatic_layout).then(|| position.get()) { self.attributes.position = Some(winit::dpi::Position::Physical(initial_position.into())); } if let Value::Dynamic(position) = position { self.outer_position = Some(position); } self } /// Sets `position` to be a dynamic synchronized with this window's inner /// position. /// /// When the window is moved, this dynamic will contain its new position. /// Setting this dynamic to a new value has no effect. To move a window, use /// [`outer_position`](Self::outer_position). pub fn inner_position(mut self, position: impl IntoDynamic>) -> Self { self.inner_position = Some(position.into_dynamic()); self } /// Resizes this window to fit the contents when `resize_to_fit` is true. pub fn resize_to_fit(mut self, resize_to_fit: impl IntoValue) -> Self { self.resize_to_fit = resize_to_fit.into_value(); self } /// Prevents the window contents from being captured by other apps. pub fn content_protected(mut self, protected: impl IntoValue) -> Self { let protected = protected.into_value(); self.attributes.content_protected = protected.get(); self.content_protected = Some(protected); self } /// Controls whether the cursor should interact with this window or not. pub fn cursor_hittest(mut self, hittest: impl IntoValue) -> Self { self.cursor_hittest = Some(hittest.into_value()); self } /// Sets whether the cursor is visible when above this window. pub fn cursor_visible(mut self, visible: impl IntoValue) -> Self { self.cursor_visible = Some(visible.into_value()); self } /// A dynamic providing access to the window coordinate of the cursor, or /// -1, -1 if the cursor is not currently hovering the window. /// /// In the future, this dynamic will also support setting the position of /// the cursor within the window. pub fn cursor_position(mut self, window_position: impl IntoDynamic>) -> Self { self.cursor_position = Some(window_position.into_dynamic()); self } /// Controls whether window decorations are shown around this window. pub fn decorated(mut self, decorated: impl IntoValue) -> Self { let decorated = decorated.into_value(); self.attributes.decorations = decorated.get(); self.decorated = Some(decorated); self } /// Sets the enabled buttons for this window. pub fn enabled_buttons(mut self, buttons: impl IntoValue) -> Self { let buttons = buttons.into_value(); self.attributes.enabled_buttons = buttons.get(); self.enabled_buttons = Some(buttons); self } /// Controls the level of this window. pub fn window_level(mut self, window_level: impl IntoValue) -> Self { let window_level = window_level.into_value(); self.attributes.window_level = window_level.get(); self.window_level = Some(window_level); self } /// Provides a dynamic that is updated with the minimized status of this /// window. pub fn minimized(mut self, minimized: impl IntoDynamic) -> Self { self.minimized = Some(minimized.into_dynamic()); self } /// Provides a dynamic that is updated with the maximized status of this /// window. pub fn maximized(mut self, maximized: impl IntoDynamic) -> Self { let maximized = maximized.into_dynamic(); self.attributes.maximized = maximized.get(); self.maximized = Some(maximized); self } /// Controls whether the window is resizable by the user or not. pub fn resizable(mut self, resizable: impl IntoValue) -> Self { let resizable = resizable.into_value(); self.attributes.resizable = resizable.get(); self.resizable = Some(resizable); self } /// Controls the increments in which the window can be resized. pub fn resize_increments(mut self, resize_increments: impl IntoValue>) -> Self { self.resize_increments = Some(resize_increments.into_value()); self } /// Sets this window to render with a transparent background. pub fn transparent(mut self) -> Self { self.attributes.transparent = true; self } /// Controls the visibility of this window. pub fn visible(mut self, visible: impl IntoDynamic) -> Self { let visible = visible.into_dynamic(); self.attributes.visible = visible.get(); self.visible = Some(visible); self } /// Sets this window's `zoom` factor. /// /// The zoom factor is multiplied with the DPI scaling from the window /// server to allow an additional scaling factor to be applied. pub fn zoom(mut self, zoom: impl IntoDynamic) -> Self { self.zoom = Some(zoom.into_dynamic().map_each_into()); self } /// Sets the [`ThemeMode`] for this window. /// /// If a [`ThemeMode`] is provided, the window will be set to this theme /// mode upon creation and will not be updated while the window is running. /// /// If a [`Dynamic`] is provided, the initial value will be ignored and the /// dynamic will be updated when the window opens with the user's current /// theme mode. The dynamic will also be updated any time the user's theme /// mode changes. /// /// Setting the [`Dynamic`]'s value will also update the window with the new /// mode until a mode change is detected, upon which the new mode will be /// stored. pub fn themed_mode(mut self, theme_mode: impl IntoValue) -> Self { self.theme_mode = Some(theme_mode.into_value()); self } /// Applies `theme` to the widgets in this window. pub fn themed(mut self, theme: impl IntoValue) -> Self { self.theme = theme.into_value(); self } /// Adds `font_data` to the list of fonts to load for availability when /// rendering. /// /// All font families contained in `font_data` will be loaded. pub fn loading_font(self, font_data: Vec) -> Self { self.fonts.push(font_data); self } /// Invokes `on_open` when this window is first opened, even if it is not /// visible. pub fn on_open(mut self, on_open: Function) -> Self where Function: FnOnce(WindowHandle) + Send + 'static, { self.on_open = Some(OnceCallback::new(on_open)); self } /// Invokes `on_init` before the window initialization begins. pub fn on_init(mut self, on_init: Function) -> Self where Function: FnOnce(&winit::window::Window) + Send + 'static, { self.on_init = Some(PreShowCallback(Box::new(Some(on_init)))); self } /// Invokes `on_close` when this window is closed. pub fn on_close(mut self, on_close: Function) -> Self where Function: FnOnce() + Send + 'static, { self.on_closed = Some(OnceCallback::new(|()| on_close())); self } /// Invokes `on_close_requested` when the window is requested to be closed. /// /// If the function returns true, the window is allowed to be closed, /// otherwise the window remains open. pub fn on_close_requested(mut self, on_close_requested: Function) -> Self where Function: FnMut(()) -> bool + Send + 'static, { self.close_requested = Some(SharedCallback::new(on_close_requested)); self } /// Invokes `on_file_drop` when a file is hovered or dropped on this window. pub fn on_file_drop(mut self, on_file_drop: Function) -> Self where Function: FnMut(FileDrop) + Send + 'static, { self.on_file_drop = Some(Callback::new(on_file_drop)); self } /// Sets the window's title. pub fn titled(mut self, title: impl IntoValue) -> Self { self.title = title.into_value(); self } /// Sets the window's icon. pub fn icon(mut self, icon: impl IntoValue>) -> Self { self.icon = Some(icon.into_value()); self } /// Sets `modifiers` to contain the state of the keyboard modifiers when /// this window has keyboard focus. pub fn modifiers(mut self, modifiers: impl IntoDynamic) -> Self { self.modifiers = Some(modifiers.into_dynamic()); self } /// Sets the name of the application. /// /// - `WM_CLASS` on X11 /// - application ID on wayland /// - class name on windows pub fn app_name(mut self, name: impl Into) -> Self { self.attributes.app_name = Some(name.into()); self } /// Invokes `callback` when `key` is pressed while `modifiers` are pressed. /// /// Widgets have a chance to handle keyboard input before the Window. pub fn with_shortcut( mut self, key: impl Into, modifiers: ModifiersState, callback: F, ) -> Self where F: FnMut(KeyEvent) -> EventHandling + Send + 'static, { self.shortcuts .map_mut(|mut shortcuts| shortcuts.insert(key, modifiers, callback)); self } /// Invokes `shortcuts` when keyboard input is unhandled in this window. pub fn with_shortcuts(mut self, shortcuts: impl IntoValue) -> Self { self.shortcuts = shortcuts.into_value(); self } /// Invokes `callback` when `key` is pressed while `modifiers` are pressed. /// If the shortcut is held, the callback will be invoked on repeat events. /// /// Widgets have a chance to handle keyboard input before the Window. pub fn with_repeating_shortcut( mut self, key: impl Into, modifiers: ModifiersState, callback: F, ) -> Self where F: FnMut(KeyEvent) -> EventHandling + Send + 'static, { self.shortcuts .map_mut(|mut shortcuts| shortcuts.insert_repeating(key, modifiers, callback)); self } } impl Run for Window where Behavior: WindowBehavior, { fn run(self) -> crate::Result { let mut app = PendingApp::default(); self.open(&mut app)?; app.run() } } impl Open for T where T: MakeWindow, { fn open(self, app: &mut App) -> crate::Result where App: Application + ?Sized, { let this = self.make_window(); let app_app = app.as_app(); let handle = this.pending.handle(); OpenWindow::::open_with( app, sealed::Context { user: this.context, settings: RefCell::new(sealed::WindowSettings { app: app_app, title: this.title, redraw_status: this.pending.0.redraw_status.clone(), on_open: this.on_open, on_init: this.on_init, on_closed: this.on_closed, transparent: this.attributes.transparent, attributes: Some(this.attributes), occluded: this.occluded.unwrap_or_default(), focused: this.focused.unwrap_or_default(), inner_size: this.inner_size.unwrap_or_default(), theme: Some(this.theme), theme_mode: this.theme_mode, font_data_to_load: this.fonts, serif_font_family: this.serif_font_family, sans_serif_font_family: this.sans_serif_font_family, fantasy_font_family: this.fantasy_font_family, monospace_font_family: this.monospace_font_family, cursive_font_family: this.cursive_font_family, vsync: this.vsync, multisample_count: this.multisample_count, close_requested: this.close_requested, zoom: this.zoom.unwrap_or_else(|| Dynamic::new(Fraction::ONE)), resize_to_fit: this.resize_to_fit, content_protected: this.content_protected.unwrap_or_default(), cursor_hittest: this.cursor_hittest.unwrap_or_else(|| Value::Constant(true)), cursor_visible: this.cursor_visible.unwrap_or_else(|| Value::Constant(true)), cursor_position: this.cursor_position.unwrap_or_default(), window_level: this.window_level.unwrap_or_default(), decorated: this.decorated.unwrap_or_else(|| Value::Constant(true)), maximized: this.maximized.unwrap_or_default(), minimized: this.minimized.unwrap_or_default(), resizable: this.resizable.unwrap_or_else(|| Value::Constant(true)), resize_increments: this.resize_increments.unwrap_or_default(), visible: this.visible.unwrap_or_default(), inner_position: this.inner_position.unwrap_or_default(), outer_position: this.outer_position.unwrap_or_default(), outer_size: this.outer_size.unwrap_or_default(), window_icon: this.icon.unwrap_or_default(), modifiers: this.modifiers.unwrap_or_default(), enabled_buttons: this .enabled_buttons .unwrap_or(Value::Constant(WindowButtons::all())), fullscreen: this.fullscreen.unwrap_or_default(), shortcuts: this.shortcuts, on_file_drop: this.on_file_drop, }), pending: this.pending, }, )?; Ok(handle) } fn run_in(self, mut app: PendingApp) -> crate::Result { self.open(&mut app)?; app.run() } } /// A type that can be made into a [`Window`]. pub trait MakeWindow { /// The behavior associated with this window. type Behavior: WindowBehavior; /// Returns a new window from `self`. fn make_window(self) -> Window; /// Opens `self` in the center of the monitor the window initially appears /// on. fn open_centered(self, app: &mut App) -> crate::Result where Self: Sized, App: Application + ?Sized, { self.make_window().open_centered(app) } /// Runs `self` in the center of the monitor the window /// initially appears on. fn run_centered(self) -> crate::Result where Self: Sized, { self.make_window().run() } /// Runs `app` after opening `self` in the center of the monitor the window /// initially appears on. fn run_centered_in(self, mut app: PendingApp) -> crate::Result where Self: Sized, { self.make_window().open_centered(&mut app)?; app.run() } } impl MakeWindow for Window where Behavior: WindowBehavior, { type Behavior = Behavior; fn make_window(self) -> Window { self } } impl MakeWindow for T where T: MakeWidget, { type Behavior = WidgetInstance; fn make_window(self) -> Window { Window::for_widget(self.make_widget()) } } /// A file drop event for a window. pub struct FileDrop { /// The handle to the window the file drop event is for. pub window: WindowHandle, /// The file drop event. pub drop: DropEvent, } /// A drop event. pub enum DropEvent { /// The payload is being hovered over the container. Hover(T), /// The payload has been dropped on the container. Dropped(T), /// The payload previously hovered has been cancelled. Cancelled, } impl DropEvent { /// Returns the payload of this event. #[must_use] pub fn payload(&self) -> Option<&T> { match self { Self::Hover(t) | Self::Dropped(t) => Some(t), Self::Cancelled => None, } } } /// The behavior of a Cushy window. pub trait WindowBehavior: Sized + 'static { /// The type that is provided when initializing this window. type Context: Send + 'static; /// Return a new instance of this behavior using `context`. fn initialize( window: &mut RunningWindow>, context: Self::Context, ) -> Self; /// Create the window's root widget. This function is only invoked once. fn make_root(&mut self) -> WidgetInstance; /// Invoked once the window has been fully initialized. #[allow(unused_variables)] fn initialized(&mut self, window: &mut W) where W: PlatformWindow, { } /// The window has been requested to close. If this function returns true, /// the window will be closed. Returning false prevents the window from /// closing. #[allow(unused_variables)] fn close_requested(&self, window: &mut W) -> bool where W: PlatformWindow, { true } /// Runs this behavior as an application. fn run() -> crate::Result where Self::Context: Default, { Self::run_with(::default()) } /// Runs this behavior as an application, initialized with `context`. fn run_with(context: Self::Context) -> crate::Result { Window::::new(context).run() } } #[allow(clippy::struct_excessive_bools)] struct OpenWindow { behavior: T, tree: Tree, root: MountedWidget, contents: Drawing, cursor: CursorState, mouse_buttons: AHashMap>, redraw_status: InvalidationStatus, initial_frame: bool, occluded: Dynamic, focused: Dynamic, inner_size: Tracked>>, outer_size: Dynamic>, keyboard_activated: Option, min_inner_size: Option>, max_inner_size: Option>, resize_to_fit: Value, theme: Option>, current_theme: ThemePair, theme_mode: Value, transparent: bool, fonts: FontState, app: App, on_closed: Option, vsync: bool, dpi_scale: Dynamic, zoom: Tracked>, close_requested: Option>, content_protected: Tracked>, cursor_hittest: Tracked>, cursor_visible: Tracked>, cursor_position: Tracked>>, window_level: Tracked>, decorated: Tracked>, maximized: Tracked>, minimized: Tracked>, resizable: Tracked>, resize_increments: Tracked>>, visible: Tracked>, outer_position: Tracked>>, inner_position: Dynamic>, window_icon: Tracked>>, enabled_buttons: Tracked>, fullscreen: Tracked>>, modifiers: Dynamic, shortcuts: Value, on_file_drop: Option>, } impl OpenWindow where T: WindowBehavior, { fn request_close( behavior: &mut T, window: &mut RunningWindow>, ) -> bool { behavior.close_requested(window) && window .close_requested .as_ref() .map_or(true, |close| close.invoke(())) } fn keyboard_activate_widget( &mut self, is_pressed: bool, widget: Option, window: &mut W, kludgine: &mut Kludgine, ) where W: PlatformWindow, { if is_pressed { if let Some(default) = widget.and_then(|id| self.tree.widget_from_node(id)) { if let Some(previously_active) = self .keyboard_activated .take() .and_then(|id| self.tree.widget(id)) { EventContext::new( WidgetContext::new( previously_active, &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ) .deactivate(); } EventContext::new( WidgetContext::new( default.clone(), &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ) .activate(); self.keyboard_activated = self.tree.active_widget_id(); } } else if let Some(keyboard_activated) = self .keyboard_activated .take() .and_then(|id| self.tree.widget(id)) { EventContext::new( WidgetContext::new( keyboard_activated, &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ) .deactivate(); } } fn constrain_window_resizing( &mut self, resizable: bool, window: &mut RunningWindow, graphics: &mut kludgine::Graphics<'_>, ) -> RootMode where W: PlatformWindowImplementation, { let mut root_or_child = self.root.widget.clone(); let mut root_mode = None; let mut padding = Edges::::default(); loop { let Some(managed) = self.tree.widget(root_or_child.id()) else { break; }; let mut context = EventContext::new( WidgetContext::new( managed, &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), graphics, ); let mut widget = root_or_child.lock(); match widget.as_widget().root_behavior(&mut context) { Some((behavior, child)) => { let child = child.clone(); match behavior { RootBehavior::PassThrough => {} RootBehavior::Expand => { root_mode = root_mode.or(Some(RootMode::Expand)); } RootBehavior::Align => { root_mode = root_mode.or(Some(RootMode::Align)); } RootBehavior::Pad(edges) => { padding += edges.into_px(context.kludgine.scale()); } RootBehavior::Resize(range) => { let padding = padding.size(); let min_width = range .width .minimum() .map_or(Px::ZERO, |width| width.into_px(context.kludgine.scale())) .saturating_add(padding.width); let max_width = range .width .maximum() .map_or(Px::MAX, |width| width.into_px(context.kludgine.scale())) .saturating_add(padding.width); let min_height = range .height .minimum() .map_or(Px::ZERO, |height| height.into_px(context.kludgine.scale())) .saturating_add(padding.height); let max_height = range .height .maximum() .map_or(Px::MAX, |height| height.into_px(context.kludgine.scale())) .saturating_add(padding.height); let new_min_size = (min_width > 0 || min_height > 0) .then_some(Size::new(min_width, min_height).into_unsigned()); if new_min_size != self.min_inner_size && resizable { context.set_min_inner_size(new_min_size); self.min_inner_size = new_min_size; } let new_max_size = (max_width > 0 || max_height > 0) .then_some(Size::new(max_width, max_height).into_unsigned()); if new_max_size != self.max_inner_size && resizable { context.set_max_inner_size(new_max_size); } self.max_inner_size = new_max_size; break; } } drop(widget); root_or_child = child.clone(); } None => break, } } root_mode.unwrap_or(RootMode::Fit) } fn load_fonts( settings: &mut sealed::WindowSettings, app_fonts: FontCollection, fontdb: &mut fontdb::Database, ) -> FontState { let fonts = FontState::new(fontdb, settings.font_data_to_load.clone(), app_fonts); fonts.apply_font_family_list( &settings.serif_font_family, || default_family(Family::Serif), |name| fontdb.set_serif_family(name), ); fonts.apply_font_family_list( &settings.sans_serif_font_family, || { let bundled_font_name; #[cfg(feature = "roboto-flex")] { bundled_font_name = Some(String::from("Roboto Flex")); } #[cfg(not(feature = "roboto-flex"))] { bundled_font_name = None; } bundled_font_name.map_or_else( || default_family(Family::SansSerif), |name| Some(FamilyOwned::Name(name)), ) }, |name| fontdb.set_sans_serif_family(name), ); fonts.apply_font_family_list( &settings.fantasy_font_family, || default_family(Family::Fantasy), |name| fontdb.set_fantasy_family(name), ); fonts.apply_font_family_list( &settings.monospace_font_family, || default_family(Family::Monospace), |name| fontdb.set_monospace_family(name), ); fonts.apply_font_family_list( &settings.cursive_font_family, || default_family(Family::Cursive), |name| fontdb.set_cursive_family(name), ); fonts } fn handle_window_keyboard_input( &mut self, window: &mut W, kludgine: &mut Kludgine, input: KeyEvent, ) -> EventHandling where W: PlatformWindow, { match input.logical_key { Key::Character(ch) if ch == "w" && window.modifiers().primary() => { if !input.repeat && input.state.is_pressed() && self.behavior.close_requested(window) { window.close(); window.set_needs_redraw(); } HANDLED } Key::Named(NamedKey::Space) if !window.modifiers().possible_shortcut() => { let target = self.tree.focused_widget().unwrap_or(self.root.node_id); let target = self.tree.widget_from_node(target).expect("missing widget"); let mut target = EventContext::new( WidgetContext::new( target, &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); match input.state { ElementState::Pressed => { if target.active() { target.deactivate(); target.apply_pending_state(); } target.activate(); } ElementState::Released => { target.deactivate(); } } HANDLED } Key::Named(NamedKey::Tab) if !window.modifiers().possible_shortcut() => { if input.state.is_pressed() { let reverse = window.modifiers().state().shift_key(); let target = self.tree.focused_widget().unwrap_or(self.root.node_id); let target = self.tree.widget_from_node(target).expect("missing widget"); let mut target = EventContext::new( WidgetContext::new( target, &self.current_theme, window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); if reverse { target.return_focus(); } else { target.advance_focus(); } } HANDLED } Key::Named(NamedKey::Enter) => { self.keyboard_activate_widget( input.state.is_pressed(), self.tree.default_widget(), window, kludgine, ); HANDLED } Key::Named(NamedKey::Escape) => { self.keyboard_activate_widget( input.state.is_pressed(), self.tree.escape_widget(), window, kludgine, ); HANDLED } _ => { tracing::event!( Level::DEBUG, logical = ?input.logical_key, physical = ?input.physical_key, state = ?input.state, "Ignored Keyboard Input", ); IGNORED } } } #[allow(clippy::needless_pass_by_value)] fn new( mut behavior: T, mut window: W, graphics: &mut kludgine::Graphics<'_>, mut settings: sealed::WindowSettings, ) -> Self where W: PlatformWindowImplementation, { let redraw_status = settings.redraw_status.clone(); if let Value::Dynamic(title) = &settings.title { let handle = window.handle(redraw_status.clone()); title .for_each_cloned(move |title| { handle.inner.send(WindowCommand::SetTitle(title)); }) .persist(); } let app = settings.app.clone(); let fonts = Self::load_fonts( &mut settings, app.cushy().fonts.clone(), graphics.font_system().db_mut(), ); let dpi_scale = Dynamic::new(graphics.dpi_scale()); settings.inner_position.set(window.inner_position()); settings.outer_position.set(window.outer_position()); let theme_mode = match settings.theme_mode.take() { Some(Value::Dynamic(dynamic)) => { dynamic.set(window.theme().into()); Value::Dynamic(dynamic) } Some(Value::Constant(mode)) => Value::Constant(mode), None => Value::dynamic(window.theme().into()), }; let tree = Tree::default(); let root = tree.push_boxed(behavior.make_root(), None); let theme = settings.theme.unwrap_or_default(); let (current_theme, theme) = match theme { Value::Constant(theme) => (theme, None), Value::Dynamic(dynamic) => (dynamic.get(), Some(dynamic.into_reader())), }; if let Some(on_open) = settings.on_open { let handle = window.handle(redraw_status.clone()); on_open.invoke(handle); } let mut this = Self { behavior, root, tree, contents: Drawing::default(), cursor: CursorState { location: None, widget: None, }, mouse_buttons: AHashMap::default(), redraw_status, initial_frame: true, occluded: settings.occluded, focused: settings.focused, inner_size: Tracked::from(settings.inner_size).ignoring_first(), keyboard_activated: None, min_inner_size: None, max_inner_size: None, resize_to_fit: settings.resize_to_fit, current_theme, theme, theme_mode, transparent: settings.transparent, fonts, app, on_closed: settings.on_closed, vsync: settings.vsync, close_requested: settings.close_requested, dpi_scale, zoom: Tracked::from(settings.zoom), content_protected: Tracked::from(settings.content_protected).ignoring_first(), cursor_hittest: Tracked::from(settings.cursor_hittest), cursor_visible: Tracked::from(settings.cursor_visible), cursor_position: Tracked::from(settings.cursor_position), window_level: Tracked::from(settings.window_level).ignoring_first(), decorated: Tracked::from(settings.decorated).ignoring_first(), maximized: Tracked::from(settings.maximized), minimized: Tracked::from(settings.minimized), resizable: Tracked::from(settings.resizable).ignoring_first(), resize_increments: Tracked::from(settings.resize_increments), visible: Tracked::from(settings.visible).ignoring_first(), outer_size: settings.outer_size, inner_position: settings.inner_position, outer_position: Tracked::from(settings.outer_position).ignoring_first(), window_icon: Tracked::from(settings.window_icon), modifiers: settings.modifiers, enabled_buttons: Tracked::from(settings.enabled_buttons).ignoring_first(), fullscreen: Tracked::from(settings.fullscreen).ignoring_first(), shortcuts: settings.shortcuts, on_file_drop: settings.on_file_drop, }; this.synchronize_platform_window(&mut window); // Perform an initial layout. this.prepare(window, graphics); this } fn new_frame(&mut self, graphics: &mut kludgine::Graphics<'_>) { if let Some(theme) = &mut self.theme { if theme.has_updated() { self.current_theme = theme.get(); self.root.invalidate(); } } self.redraw_status.refresh_received(); graphics.reset_text_attributes(); if let Some(zoom) = self.zoom.updated() { graphics.set_zoom(*zoom); self.redraw_status.invalidate(self.root.id()); } self.tree .new_frame(self.redraw_status.invalidations().drain()); } fn prepare(&mut self, mut window: W, graphics: &mut kludgine::Graphics<'_>) where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); self.synchronize_platform_window(&mut window); self.new_frame(graphics); let resize_to_fit = self.resize_to_fit.get(); let resizable = window.is_resizable() || resize_to_fit; let mut window = RunningWindow::new( window, graphics.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics); let fonts_changed = self.fonts.next_frame(graphics.font_system().db_mut()); if fonts_changed { graphics.rebuild_font_system(); } let graphics = self.contents.new_frame(graphics); let mut context = GraphicsContext { widget: WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), gfx: Exclusive::Owned(Graphics::new(graphics)), }; if self.initial_frame { self.root .lock() .as_widget() .mounted(&mut context.as_event_context()); } self.theme_mode.redraw_when_changed(&context); let mut layout_context = LayoutContext::new(&mut context); let window_size = layout_context.gfx.size(); if !self.transparent { let background_color = layout_context.theme().surface.color; layout_context.graphics.gfx.fill(background_color); } let layout_size = layout_context.layout(if matches!(root_mode, RootMode::Expand | RootMode::Align) { window_size.map(ConstraintLimit::Fill) } else { window_size.map(ConstraintLimit::SizeToFit) }); let actual_size = if root_mode == RootMode::Align { window_size.max(layout_size) } else { layout_size }; let render_size = actual_size.min(window_size); layout_context.invalidate_when_changed(&self.inner_size); layout_context.invalidate_when_changed(&self.resize_to_fit); self.root.set_layout(Rect::from(render_size.into_signed())); if self.initial_frame { self.initial_frame = false; self.root .lock() .as_widget() .mounted(&mut layout_context.as_event_context()); layout_context.focus(); layout_context.as_event_context().apply_pending_state(); } if render_size.width < window_size.width || render_size.height < window_size.height { layout_context .clipped_to(Rect::from(render_size.into_signed())) .redraw(); } else { layout_context.redraw(); } let new_size = if let Some(new_size) = self.inner_size.updated() { layout_context.request_inner_size(*new_size) } else if actual_size != window_size && !resizable { let mut new_size = actual_size; if let Some(min_size) = self.min_inner_size { new_size = new_size.max(min_size); } if let Some(max_size) = self.max_inner_size { new_size = new_size.min(max_size); } layout_context.request_inner_size(new_size) } else if resize_to_fit && window_size != layout_size { layout_context.request_inner_size(layout_size) } else { None }; if let Some(new_size) = new_size { self.inner_size.set_and_read(new_size); self.outer_size.set(layout_context.window().outer_size()); self.root.invalidate(); } layout_context.as_event_context().update_hovered_widget(); } fn close_requested(&mut self, window: W, kludgine: &mut Kludgine) -> bool where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); if self.behavior.close_requested(&mut window) { window.close(); true } else { false } } fn resized(&mut self, new_size: Size, window: &W) where W: PlatformWindowImplementation, { self.inner_size.set_and_read(new_size); self.outer_size.set(window.outer_size()); self.update_ized(window); self.root.invalidate(); } fn moved(&mut self, new_inner_position: Point, new_outer_position: Point) { self.outer_position.set_and_read(new_outer_position); self.inner_position.set(new_inner_position); } fn update_ized(&mut self, window: &W) where W: PlatformWindowImplementation, { if let Some(winit) = window.winit() { // TODO should these be supported outside of winit? Put in a feature // request if you read this and need them. self.maximized.set_and_read(winit.is_maximized()); if let Some(minimized) = winit.is_minimized() { self.minimized.set_and_read(minimized); } self.decorated.set_and_read(winit.is_decorated()); } } fn synchronize_platform_window(&mut self, window: &mut W) where W: PlatformWindowImplementation, { macro_rules! when_updated { ($prop:ident, $handle:ident, $block:expr) => { self.$prop.inner_sync_when_changed($handle.clone()); if let Some($prop) = self.$prop.updated() { $block } }; } self.redraw_status.sync_received(); self.update_ized(window); if let Some(winit) = window.winit() { let mut redraw = false; let handle = window.handle(self.redraw_status.clone()); when_updated!(outer_position, handle, { winit.set_outer_position(PhysicalPosition::::from(*outer_position)); }); when_updated!(content_protected, handle, { winit.set_content_protected(*content_protected); }); when_updated!(cursor_hittest, handle, { let _ = winit.set_cursor_hittest(*cursor_hittest); }); when_updated!(cursor_visible, handle, { winit.set_cursor_visible(*cursor_visible); }); when_updated!(window_level, handle, { winit.set_window_level(*window_level); }); when_updated!(decorated, handle, { winit.set_decorations(*decorated); }); when_updated!(resize_increments, handle, { let increments: Option> = if resize_increments.width > 0 || resize_increments.height > 0 { Some(PhysicalSize::new( resize_increments.width.into_float(), resize_increments.height.into_float(), )) } else { None }; winit.set_resize_increments(increments); }); when_updated!(visible, handle, { winit.set_visible(*visible); }); when_updated!(resizable, handle, { winit.set_resizable(*resizable); redraw = true; }); when_updated!(window_icon, handle, { let icon = window_icon.as_ref().map(|icon| { Icon::from_rgba(icon.as_raw().clone(), icon.width(), icon.height()) .expect("valid image") }); winit.set_window_icon(icon); }); when_updated!(enabled_buttons, handle, { winit.set_enabled_buttons(*enabled_buttons); }); when_updated!(fullscreen, handle, { winit.set_fullscreen(fullscreen.clone()); }); if redraw { window.set_needs_redraw(); } } } pub fn set_focused(&mut self, focused: bool) { self.focused.set(focused); } pub fn set_occluded(&mut self, window: &W, occluded: bool) where W: PlatformWindowImplementation, { self.occluded.set(occluded); self.update_ized(window); } pub fn keyboard_input( &mut self, window: W, kludgine: &mut Kludgine, device_id: DeviceId, input: KeyEvent, is_synthetic: bool, ) -> EventHandling where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let target = self.tree.focused_widget().unwrap_or(self.root.node_id); let Some(target) = self.tree.widget_from_node(target) else { return IGNORED; }; let mut target = EventContext::new( WidgetContext::new( target, &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); if recursively_handle_event(&mut target, |widget| { widget.keyboard_input(device_id, input.clone(), is_synthetic) }) .is_some() { return HANDLED; } if self .shortcuts .map(|shortcuts| shortcuts.input(input.clone())) .is_break() { return HANDLED; } drop(target); self.handle_window_keyboard_input(&mut window, kludgine, input) } pub fn mouse_wheel( &mut self, window: W, kludgine: &mut Kludgine, device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, ) -> EventHandling where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let widget = self .tree .hovered_widget() .and_then(|hovered| self.tree.widget_from_node(hovered)) .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); let mut widget = EventContext::new( WidgetContext::new( widget, &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); if recursively_handle_event(&mut widget, |widget| { widget.mouse_wheel(device_id, delta, phase) }) .is_some() { HANDLED } else { IGNORED } } fn ime(&mut self, window: W, kludgine: &mut Kludgine, ime: &Ime) -> EventHandling where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let widget = self .tree .focused_widget() .and_then(|hovered| self.tree.widget_from_node(hovered)) .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); let mut target = EventContext::new( WidgetContext::new( widget, &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); if recursively_handle_event(&mut target, |widget| widget.ime(ime.clone())).is_some() { HANDLED } else { IGNORED } } fn cursor_moved( &mut self, window: W, kludgine: &mut Kludgine, device_id: DeviceId, position: impl Into>, ) where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let location = position.into(); self.cursor.location = Some(location); self.cursor_position.set_and_read(location); EventContext::new( WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ) .update_hovered_widget(); if let Some(state) = self.mouse_buttons.get(&device_id) { // Mouse Drag for (button, handler) in state { let Some(handler) = self.tree.widget(*handler) else { continue; }; let mut context = EventContext::new( WidgetContext::new( handler.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); let Some(last_rendered_at) = context.last_layout() else { continue; }; context.mouse_drag(location - last_rendered_at.origin, device_id, *button); } } } fn cursor_left(&mut self, window: W, kludgine: &mut Kludgine) where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); self.cursor.location = None; self.cursor_position .set_and_read(Point::squared(Px::new(-1))); if self.cursor.widget.take().is_some() { let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let mut context = EventContext::new( WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); context.clear_hover(); } } fn mouse_input( &mut self, window: W, kludgine: &mut Kludgine, device_id: DeviceId, state: ElementState, button: MouseButton, ) -> EventHandling where W: PlatformWindowImplementation, { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); match state { ElementState::Pressed => { if let (ElementState::Pressed, Some(location), Some(hovered)) = ( state, self.cursor.location, self.cursor.widget.and_then(|id| self.tree.widget(id)), ) { if let Some(handler) = recursively_handle_event( &mut EventContext::new( WidgetContext::new( hovered.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ), |context| { let Some(layout) = context.last_layout() else { return IGNORED; }; let relative = location - layout.origin; context.mouse_down(relative, device_id, button) }, ) { self.mouse_buttons .entry(device_id) .or_default() .insert(button, handler.id()); return HANDLED; } } else { EventContext::new( WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ) .clear_focus(); } IGNORED } ElementState::Released => { let Some(device_buttons) = self.mouse_buttons.get_mut(&device_id) else { return IGNORED; }; let Some(handler) = device_buttons.remove(&button) else { return IGNORED; }; if device_buttons.is_empty() { self.mouse_buttons.remove(&device_id); } let Some(handler) = self.tree.widget(handler) else { return IGNORED; }; let cursor_location = self.cursor.location; let mut context = EventContext::new( WidgetContext::new( handler, &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); let relative = if let (Some(last_rendered), Some(location)) = (context.last_layout(), cursor_location) { Some(location - last_rendered.origin) } else { None }; context.mouse_up(relative, device_id, button); HANDLED } } } fn handle_drop( &mut self, drop: DropEvent, window: &kludgine::app::Window<'_, WindowCommand>, ) { if let Some(on_file_drop) = &mut self.on_file_drop { on_file_drop.invoke(FileDrop { window: WindowHandle::new(window.handle(), self.redraw_status.clone()), drop, }); } } } #[derive(Clone, Copy, Eq, PartialEq, Debug)] enum RootMode { Fit, Expand, Align, } impl kludgine::app::WindowBehavior for OpenWindow where T: WindowBehavior, { type Context = sealed::Context; fn pre_initialize(context: &Self::Context, winit: &winit::window::Window) { let Some(mut on_init) = context.settings.borrow_mut().on_init.take() else { return; }; on_init.0.pre_show(winit); } fn initialize( window: kludgine::app::Window<'_, WindowCommand>, graphics: &mut kludgine::Graphics<'_>, context: Self::Context, ) -> Self { context.pending.opened(window.handle()); let settings = context.settings.borrow(); let cushy = settings.app.cushy().clone(); let _guard = cushy.enter_runtime(); let mut window = RunningWindow::new( window, graphics.id(), &settings.redraw_status, &settings.app, &settings.focused, &settings.occluded, &settings.inner_size, &settings.close_requested, ); drop(settings); let behavior = T::initialize(&mut window, context.user); Self::new( behavior, window.window, graphics, context.settings.into_inner(), ) } fn initialized( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, ) { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); self.focused.set(window.focused()); self.occluded.set(window.occluded()); let inner_size = window.inner_size(); self.resized(inner_size, &window); self.behavior.initialized(&mut RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, )); } fn prepare( &mut self, window: kludgine::app::Window<'_, WindowCommand>, graphics: &mut kludgine::Graphics<'_>, ) { self.prepare(window, graphics); } fn present_mode(&self) -> wgpu::PresentMode { if self.vsync { wgpu::PresentMode::AutoVsync } else { wgpu::PresentMode::AutoNoVsync } } fn multisample_count(context: &Self::Context) -> std::num::NonZeroU32 { context.settings.borrow().multisample_count } fn memory_hints(_context: &Self::Context) -> wgpu::MemoryHints { wgpu::MemoryHints::MemoryUsage } fn focus_changed( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.set_focused(window.focused()); } fn occlusion_changed( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.set_occluded(&window, window.occluded()); } fn render<'pass>( &'pass mut self, _window: kludgine::app::Window<'_, WindowCommand>, graphics: &mut kludgine::RenderingGraphics<'_, 'pass>, ) { self.contents.render(1., graphics); } fn initial_window_attributes(context: &Self::Context) -> kludgine::app::WindowAttributes { let mut settings = context.settings.borrow_mut(); let mut attrs = settings.attributes.take().expect("called more than once"); if let Some(Value::Constant(theme_mode)) = &settings.theme_mode { attrs.preferred_theme = Some((*theme_mode).into()); } attrs.title = settings.title.get(); if attrs.inner_size.is_none() { let dynamic_inner = settings.inner_size.get(); if !dynamic_inner.is_zero() { attrs.inner_size = Some(winit::dpi::Size::Physical(dynamic_inner.into())); } } attrs } fn close_requested( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, ) -> bool { let cushy = self.app.cushy().clone(); let _guard = cushy.enter_runtime(); Self::request_close( &mut self.behavior, &mut RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ), ) } // fn power_preference() -> wgpu::PowerPreference { // wgpu::PowerPreference::default() // } // fn limits(adapter_limits: wgpu::Limits) -> wgpu::Limits { // wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter_limits) // } fn clear_color(&self) -> Option { Some(if self.transparent { kludgine::Color::CLEAR_BLACK } else { kludgine::Color::BLACK }) } fn composite_alpha_mode(&self, supported_modes: &[CompositeAlphaMode]) -> CompositeAlphaMode { if self.transparent && supported_modes.contains(&CompositeAlphaMode::PreMultiplied) { CompositeAlphaMode::PreMultiplied } else { CompositeAlphaMode::Auto } } // fn focus_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} // fn occlusion_changed(&mut self, window: kludgine::app::Window<'_, ()>) {} fn scale_factor_changed( &mut self, mut window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, ) { self.dpi_scale.set(kludgine.dpi_scale()); window.set_needs_redraw(); } fn resized( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.resized(window.inner_size(), &window); } fn moved( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.moved(window.inner_position(), window.outer_position()); } fn dropped_file( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, path: PathBuf, ) { self.handle_drop(DropEvent::Dropped(path), &window); } fn hovered_file( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, path: PathBuf, ) { self.handle_drop(DropEvent::Hover(path), &window); } fn hovered_file_cancelled( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.handle_drop(DropEvent::Cancelled, &window); } // fn received_character(&mut self, window: kludgine::app::Window<'_, ()>, char: char) {} fn keyboard_input( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, device_id: winit::event::DeviceId, input: winit::event::KeyEvent, is_synthetic: bool, ) { let event = KeyEvent::from_winit(input, window.modifiers()); self.keyboard_input(window, kludgine, device_id.into(), event, is_synthetic); } fn mouse_wheel( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, device_id: winit::event::DeviceId, delta: MouseScrollDelta, phase: TouchPhase, ) { self.mouse_wheel(window, kludgine, device_id.into(), delta, phase); } fn modifiers_changed( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { self.modifiers.set(window.modifiers()); } fn ime( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, ime: Ime, ) { self.ime(window, kludgine, &ime); } fn cursor_moved( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, device_id: winit::event::DeviceId, position: PhysicalPosition, ) { self.cursor_moved(window, kludgine, device_id.into(), position); } fn cursor_left( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, _device_id: winit::event::DeviceId, ) { self.cursor_left(window, kludgine); } fn mouse_input( &mut self, window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, device_id: winit::event::DeviceId, state: ElementState, button: MouseButton, ) { self.mouse_input(window, kludgine, device_id.into(), state, button); } fn theme_changed( &mut self, window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { if let Value::Dynamic(theme_mode) = &self.theme_mode { theme_mode.set(window.theme().into()); } } fn event( &mut self, mut window: kludgine::app::Window<'_, WindowCommand>, kludgine: &mut Kludgine, event: WindowCommand, ) { match event { WindowCommand::Redraw => { window.set_needs_redraw(); } WindowCommand::Sync => { self.synchronize_platform_window(&mut window); } WindowCommand::RequestClose => { let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); if self.behavior.close_requested(&mut window) { window.close(); } } WindowCommand::SetTitle(new_title) => { window.set_title(&new_title); } WindowCommand::ResetDeadKeys => { window.winit().reset_dead_keys(); } WindowCommand::RequestUserAttention(request_type) => { window.winit().request_user_attention(request_type); } WindowCommand::Focus => { window.winit().focus_window(); } WindowCommand::Ize(ize) => { let (minimize, maximize) = match ize { Some(Ize::Maximize) => (false, true), Some(Ize::Minimize) => (true, false), None => (false, false), }; if window .winit() .is_minimized() .map_or(true, |minimized| minimized != minimize) { window.winit().set_minimized(minimize); } if window.winit().is_maximized() != maximize { window.winit().set_maximized(maximize); } } WindowCommand::Execute(func) => { let mut window = RunningWindow::new( window, kludgine.id(), &self.redraw_status, &self.app, &self.focused, &self.occluded, self.inner_size.source(), &self.close_requested, ); let mut context = EventContext::new( WidgetContext::new( self.root.clone(), &self.current_theme, &mut window, &mut self.fonts, self.theme_mode.get(), &mut self.cursor, ), kludgine, ); func.execute(&mut context); } } } // fn dropped_file( // &mut self, // window: kludgine::app::Window<'_, WindowCommand>, // kludgine: &mut Kludgine, // path: std::path::PathBuf, // ) { // } // fn hovered_file( // &mut self, // window: kludgine::app::Window<'_, WindowCommand>, // kludgine: &mut Kludgine, // path: std::path::PathBuf, // ) { // } // fn hovered_file_cancelled( // &mut self, // window: kludgine::app::Window<'_, WindowCommand>, // kludgine: &mut Kludgine, // ) { // } // fn received_character( // &mut self, // window: kludgine::app::Window<'_, WindowCommand>, // kludgine: &mut Kludgine, // char: char, // ) { // } // fn modifiers_changed( // &mut self, // window: kludgine::app::Window<'_, WindowCommand>, // kludgine: &mut Kludgine, // ) { // } } impl Drop for OpenWindow { fn drop(&mut self) { if let Some(on_closed) = self.on_closed.take() { on_closed.invoke(()); } } } fn recursively_handle_event( context: &mut EventContext<'_>, mut each_widget: impl FnMut(&mut EventContext<'_>) -> EventHandling, ) -> Option { match each_widget(context) { HANDLED => Some(context.widget().clone()), IGNORED => context.parent().and_then(|parent| { recursively_handle_event(&mut context.for_other(&parent), each_widget) }), } } #[derive(Default)] pub(crate) struct CursorState { pub(crate) location: Option>, pub(crate) widget: Option, } pub(crate) mod sealed { use std::cell::RefCell; use std::fmt::Debug; use std::num::NonZeroU32; use figures::units::{Px, UPx}; use figures::{Fraction, Point, Size}; use image::{DynamicImage, RgbaImage}; use kludgine::app::winit; use kludgine::app::winit::event::Modifiers; use kludgine::app::winit::window::{Fullscreen, UserAttentionType, WindowButtons, WindowLevel}; use kludgine::Color; use crate::context::sealed::InvalidationStatus; use crate::context::EventContext; use crate::fonts::FontCollection; use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; use crate::widget::{Callback, OnceCallback, SharedCallback}; use crate::widgets::shortcuts::ShortcutMap; use crate::window::{FileDrop, PendingWindow, ThemeMode, WindowAttributes, WindowHandle}; use crate::App; pub struct Context { pub user: C, pub pending: PendingWindow, pub settings: RefCell, } pub struct WindowSettings { pub app: App, pub redraw_status: InvalidationStatus, pub title: Value, pub attributes: Option, pub occluded: Dynamic, pub focused: Dynamic, pub inner_size: Dynamic>, pub zoom: Dynamic, pub theme: Option>, pub theme_mode: Option>, pub transparent: bool, pub serif_font_family: FontFamilyList, pub sans_serif_font_family: FontFamilyList, pub fantasy_font_family: FontFamilyList, pub monospace_font_family: FontFamilyList, pub cursive_font_family: FontFamilyList, pub font_data_to_load: FontCollection, pub on_open: Option>, pub on_init: Option, pub on_closed: Option, pub vsync: bool, pub multisample_count: NonZeroU32, pub resize_to_fit: Value, pub close_requested: Option>, pub content_protected: Value, pub cursor_hittest: Value, pub cursor_visible: Value, pub cursor_position: Dynamic>, pub window_level: Value, pub decorated: Value, pub maximized: Dynamic, pub minimized: Dynamic, pub resizable: Value, pub resize_increments: Value>, pub visible: Dynamic, pub inner_position: Dynamic>, pub outer_position: Dynamic>, pub outer_size: Dynamic>, pub window_icon: Value>, pub modifiers: Dynamic, pub enabled_buttons: Value, pub fullscreen: Value>, pub shortcuts: Value, pub on_file_drop: Option>, } pub struct WindowExecute(Box); impl WindowExecute { pub fn new(func: F) -> Self where F: FnOnce(&mut EventContext<'_>) + Send + 'static, { Self(Box::new(Some(func))) } pub fn execute(mut self, context: &mut EventContext<'_>) { self.0.execute(context); } } impl Debug for WindowExecute { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WindowExecute").finish_non_exhaustive() } } pub trait ExecuteFunc: Send + 'static { fn execute(&mut self, context: &mut EventContext<'_>); } impl ExecuteFunc for Option where F: FnOnce(&mut EventContext<'_>) + Send + 'static, { fn execute(&mut self, context: &mut EventContext<'_>) { let func = self.take().expect("not executed"); func(context); } } #[derive(Debug)] pub enum WindowCommand { Redraw, Sync, RequestClose, ResetDeadKeys, RequestUserAttention(Option), Focus, Ize(Option), SetTitle(String), Execute(WindowExecute), } #[derive(Debug, Clone)] pub enum Ize { Maximize, Minimize, } pub trait CaptureFormat { const HAS_ALPHA: bool; fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32); fn load_image(data: &[u8], size: Size) -> DynamicImage; fn pixel_color(location: Point, data: &[u8], size: Size) -> Color; } pub struct PreShowCallback(pub Box); pub trait PreShowFn: Send + 'static { fn pre_show(&mut self, winit: &winit::window::Window); } impl PreShowFn for Option where F: FnOnce(&winit::window::Window) + Send + 'static, { fn pre_show(&mut self, winit: &winit::window::Window) { let Some(this) = self.take() else { return }; this(winit); } } } /// Controls whether the light or dark theme is applied. #[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, LinearInterpolate)] pub enum ThemeMode { /// Applies the light theme Light, /// Applies the dark theme #[default] Dark, } impl ThemeMode { /// Returns the opposite mode of `self`. #[must_use] pub const fn inverse(self) -> Self { match self { ThemeMode::Light => Self::Dark, ThemeMode::Dark => Self::Light, } } /// Updates `self` with its [inverse](Self::inverse). pub fn toggle(&mut self) { *self = !*self; } } impl Not for ThemeMode { type Output = Self; fn not(self) -> Self::Output { self.inverse() } } impl From for ThemeMode { fn from(value: window::Theme) -> Self { match value { window::Theme::Light => Self::Light, window::Theme::Dark => Self::Dark, } } } impl From for window::Theme { fn from(value: ThemeMode) -> Self { match value { ThemeMode::Light => Self::Light, ThemeMode::Dark => Self::Dark, } } } impl PercentBetween for ThemeMode { fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne { if *min == *max || *self == *min { ZeroToOne::ZERO } else { ZeroToOne::ONE } } } impl Ranged for ThemeMode { const MAX: Self = Self::Dark; const MIN: Self = Self::Light; } #[cfg(any(target_os = "macos", target_os = "ios", target_os = "windows"))] fn default_family(_query: Family<'_>) -> Option { // fontdb uses system APIs to determine these defaults. None } #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))] fn default_family(query: Family<'_>) -> Option { // fontdb does not yet support configuring itself automatically. We will try // to use `fc-match` to query font config. Once this is supported, we can // remove this functionality. // let query = match query { Family::Serif => "serif", Family::SansSerif => "sans", Family::Cursive => "cursive", Family::Fantasy => "fantasy", Family::Monospace => "monospace", Family::Name(_) => return None, }; std::process::Command::new("fc-match") .arg("-f") .arg("%{family}") .arg(query) .output() .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .map(FamilyOwned::Name) } /// A handle to an open Cushy window. #[derive(Debug, Clone)] pub struct WindowHandle { inner: InnerWindowHandle, pub(crate) redraw_status: InvalidationStatus, } impl WindowHandle { pub(crate) fn new( kludgine: kludgine::app::WindowHandle, redraw_status: InvalidationStatus, ) -> Self { Self { inner: InnerWindowHandle::Known(kludgine), redraw_status, } } fn pending() -> Self { Self { inner: InnerWindowHandle::Pending(Arc::default()), redraw_status: InvalidationStatus::default(), } } /// Request that the window closes. /// /// A window may disallow itself from being closed by customizing /// [`WindowBehavior::close_requested`]. pub fn request_close(&self) { self.inner.send(sealed::WindowCommand::RequestClose); } /// Requests that the window redraws. pub fn redraw(&self) { if self.redraw_status.should_send_refresh() { self.inner.send(WindowCommand::Redraw); } } pub(crate) fn sync(&self) { if self.redraw_status.should_send_sync() { self.inner.send(WindowCommand::Sync); } } /// Marks `widget` as invalidated, and if needed, refreshes the window. pub fn invalidate(&self, widget: WidgetId) { if self.redraw_status.invalidate(widget) { self.redraw(); } } /// Executes `func` on the window thread. pub fn execute(&self, func: F) where F: FnOnce(&mut EventContext<'_>) + Send + 'static, { self.inner .send(WindowCommand::Execute(WindowExecute::new(func))); } } impl Eq for WindowHandle {} impl PartialEq for WindowHandle { fn eq(&self, other: &Self) -> bool { self.redraw_status == other.redraw_status } } impl Hash for WindowHandle { fn hash(&self, state: &mut H) { self.redraw_status.hash(state); } } #[derive(Debug, Clone)] enum InnerWindowHandle { Pending(Arc), Known(kludgine::app::WindowHandle), Virtual(WindowDynamicState), } impl InnerWindowHandle { fn send(&self, message: WindowCommand) { match self { InnerWindowHandle::Pending(pending) => { if let Some(handle) = pending.handle.get() { let _result = handle.send(message); } else { pending.commands.lock().push(message); } } InnerWindowHandle::Known(handle) => { let _result = handle.send(message); } InnerWindowHandle::Virtual(state) => match message { WindowCommand::Redraw => state.redraw_target.set(RedrawTarget::Now), WindowCommand::RequestClose => state.close_requested.set(true), WindowCommand::SetTitle(title) => state.title.set(title), WindowCommand::Execute(_func) => { tracing::error!("ignoring execution of window function on virtual window"); } WindowCommand::ResetDeadKeys | WindowCommand::RequestUserAttention(_) | WindowCommand::Focus | WindowCommand::Ize(_) | WindowCommand::Sync => {} }, }; } } /// A [`Window`] that doesn't have its root widget yet. /// /// [`PendingWindow::handle()`] returns a handle that allows code to interact /// with a window before it has had its contents initialized. This is useful, /// for example, for a button's `on_click` to be able to close the window that /// contains it. pub struct PendingWindow(WindowHandle); impl Default for PendingWindow { fn default() -> Self { Self(WindowHandle::pending()) } } impl PendingWindow { /// Returns a [`Window`] using `context` to initialize its contents. pub fn with(self, context: Behavior::Context) -> Window where Behavior: WindowBehavior, { Window::new_with_pending(context, self) } /// Returns a [`Window`] containing `root`. pub fn with_root(self, root: impl MakeWidget) -> Window { Window::new_with_pending(root.make_widget(), self) } /// Returns a [`Window`] using the default context to initialize its /// contents. pub fn using(self) -> Window where Behavior: WindowBehavior, Behavior::Context: Default, { self.with(::default()) } /// Returns a handle for this window. #[must_use] pub fn handle(&self) -> WindowHandle { self.0.clone() } fn opened(self, handle: kludgine::app::WindowHandle) -> WindowHandle { let InnerWindowHandle::Pending(pending) = &self.0.inner else { unreachable!("always pending") }; let initialized = pending.handle.set(handle.clone()); assert!(initialized.is_ok()); for command in pending.commands.lock().drain(..) { let _result = handle.send(command); } WindowHandle::new(handle, self.0.redraw_status.clone()) } } #[derive(Debug, Default)] struct PendingWindowHandle { handle: OnceLock>, commands: Mutex>, } /// A collection that stores an instance of `T` per window. /// /// This is a convenience wrapper around a `HashMap`. #[derive(Debug, Clone)] pub struct WindowLocal { by_window: AHashMap, } impl WindowLocal { /// Looks up the entry for this window. /// /// Internally this API uses [`HashMap::entry`](hash_map::HashMap::entry). pub fn entry(&mut self, context: &WidgetContext<'_>) -> hash_map::Entry<'_, KludgineId, T> { self.by_window.entry(context.kludgine_id()) } /// Sets `value` as the local value for `context`'s window. pub fn set(&mut self, context: &WidgetContext<'_>, value: T) { self.by_window.insert(context.kludgine_id(), value); } /// Looks up the value for this window, returning None if not found. /// /// Internally this API uses [`HashMap::get`](hash_map::HashMap::get). #[must_use] pub fn get(&self, context: &WidgetContext<'_>) -> Option<&T> { self.by_window.get(&context.kludgine_id()) } /// Looks up an exclusive reference to the value for this window, returning /// None if not found. /// /// Internally this API uses [`HashMap::get`](hash_map::HashMap::get). #[must_use] pub fn get_mut(&mut self, context: &WidgetContext<'_>) -> Option<&mut T> { self.by_window.get_mut(&context.kludgine_id()) } /// Removes any stored value for this window. pub fn clear_for(&mut self, context: &WidgetContext<'_>) -> Option { self.by_window.remove(&context.kludgine_id()) } /// Returns an iterator over the per-window values stored in this /// collection. #[must_use] pub fn iter(&self) -> hash_map::Iter<'_, KludgineId, T> { self.into_iter() } } impl Default for WindowLocal { fn default() -> Self { Self { by_window: AHashMap::default(), } } } impl IntoIterator for WindowLocal { type IntoIter = hash_map::IntoIter; type Item = (KludgineId, T); fn into_iter(self) -> Self::IntoIter { self.by_window.into_iter() } } impl<'a, T> IntoIterator for &'a WindowLocal { type IntoIter = hash_map::Iter<'a, KludgineId, T>; type Item = (&'a KludgineId, &'a T); fn into_iter(self) -> Self::IntoIter { self.by_window.iter() } } /// The state of a [`VirtualWindow`]. pub struct VirtualState { /// State that may be updated outside of the window's event callbacks. pub dynamic: WindowDynamicState, /// When true, this window should be closed. pub closed: bool, /// The current keyboard modifers. pub modifiers: Modifiers, /// The amount of time elapsed since the last redraw call. pub elapsed: Duration, /// The currently set cursor. pub cursor: Cursor, /// The inner size of the virtual window. pub size: Size, } impl VirtualState { fn new() -> Self { Self { dynamic: WindowDynamicState::default(), closed: false, modifiers: Modifiers::default(), elapsed: Duration::ZERO, cursor: Cursor::default(), size: Size::upx(800, 600), } } } /// Window state that is able to be updated outside of event handling, /// potentially via other threads depending on the application. #[derive(Clone, Debug, Default)] pub struct WindowDynamicState { /// The target of the next frame to draw. pub redraw_target: Dynamic, /// When true, the window has been asked to close. To ensure full Cushy /// functionality, upon detecting this, [`VirtualWindow::request_close`] /// should be invoked. pub close_requested: Dynamic, /// The current title of the window. pub title: Dynamic, } /// A target for the next redraw of a window. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum RedrawTarget { /// The window should not redraw. #[default] Never, /// The window should redraw as soon as possible. Now, /// The window should try to redraw at the given instant. At(Instant), } impl PlatformWindowImplementation for &mut VirtualState { fn close(&mut self) { self.closed = true; } fn winit(&self) -> Option<&Arc> { None } fn handle(&self, redraw_status: InvalidationStatus) -> WindowHandle { WindowHandle { inner: InnerWindowHandle::Virtual(self.dynamic.clone()), redraw_status, } } fn set_needs_redraw(&mut self) { self.dynamic.redraw_target.set(RedrawTarget::Now); } fn redraw_in(&mut self, duration: Duration) { self.redraw_at(Instant::now() + duration); } fn redraw_at(&mut self, moment: Instant) { self.dynamic.redraw_target.map_mut(|mut redraw_at| { if match *redraw_at { RedrawTarget::At(instant) => moment < instant, RedrawTarget::Never => true, RedrawTarget::Now => false, } { *redraw_at = RedrawTarget::At(moment); } }); } fn modifiers(&self) -> Modifiers { self.modifiers } fn elapsed(&self) -> Duration { self.elapsed } fn set_cursor(&mut self, cursor: Cursor) { self.cursor = cursor; } fn inner_size(&self) -> Size { self.size } fn request_inner_size(&mut self, inner_size: Size) -> Option> { self.size = inner_size; self.set_needs_redraw(); Some(inner_size) } } /// A builder that creates either a [`VirtualWindow`] or a [`CushyWindow`]. pub struct StandaloneWindowBuilder { widget: WidgetInstance, multisample_count: NonZeroU32, initial_size: Size, scale: f32, transparent: bool, zoom: Dynamic, resize_to_fit: Value, } impl StandaloneWindowBuilder { /// Returns a new builder for a standalone window that contains `contents`. #[must_use] pub fn new(contents: impl MakeWidget) -> Self { Self { widget: contents.make_widget(), multisample_count: NonZeroU32::new(4).assert("not 0"), initial_size: Size::upx(800, 600), scale: 1., zoom: Dynamic::new(Fraction::ONE), transparent: false, resize_to_fit: Value::Constant(false), } } /// Sets this window's multi-sample count. /// /// By default, 4 samples are taken. When 1 sample is used, multisampling is /// fully disabled. #[must_use] pub fn multisample_count(mut self, count: NonZeroU32) -> Self { self.multisample_count = count; self } /// Sets the size of the window. #[must_use] pub fn size(mut self, size: Size) -> Self where Unit: Into, { self.initial_size = size.map(Into::into); self } /// Sets the DPI scaling factor of the window. #[must_use] pub fn scale(mut self, scale: f32) -> Self { self.scale = scale; self } /// Sets the window not fill its background before rendering its contents. #[must_use] pub fn transparent(mut self) -> Self { self.transparent = true; self } /// Resizes this window to fit the contents when `resize_to_fit` is true. #[must_use] pub fn resize_to_fit(mut self, resize_to_fit: impl IntoValue) -> Self { self.resize_to_fit = resize_to_fit.into_value(); self } /// Returns the initialized window. #[must_use] pub fn finish(self, window: W, device: &wgpu::Device, queue: &wgpu::Queue) -> CushyWindow where W: PlatformWindowImplementation, { let mut kludgine = Kludgine::new( device, queue, wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::MultisampleState { count: self.multisample_count.get(), ..Default::default() }, self.initial_size, self.scale, ); let window = OpenWindow::::new( self.widget, window, &mut kludgine::Graphics::new(&mut kludgine, device, queue), sealed::WindowSettings { app: App::standalone(), redraw_status: InvalidationStatus::default(), title: Value::default(), attributes: None, occluded: Dynamic::default(), focused: Dynamic::default(), inner_size: Dynamic::default(), theme: None, theme_mode: None, transparent: self.transparent, serif_font_family: FontFamilyList::default(), sans_serif_font_family: FontFamilyList::default(), fantasy_font_family: FontFamilyList::default(), monospace_font_family: FontFamilyList::default(), cursive_font_family: FontFamilyList::default(), font_data_to_load: FontCollection::default(), on_open: None, on_closed: None, vsync: false, multisample_count: self.multisample_count, close_requested: None, zoom: self.zoom, resize_to_fit: self.resize_to_fit, content_protected: Value::Constant(false), cursor_hittest: Value::Constant(true), cursor_visible: Value::Constant(true), cursor_position: Dynamic::default(), window_level: Value::default(), decorated: Value::Constant(true), maximized: Dynamic::new(false), minimized: Dynamic::new(false), resizable: Value::Constant(true), resize_increments: Value::default(), visible: Dynamic::new(true), inner_position: Dynamic::default(), outer_position: Dynamic::default(), outer_size: Dynamic::default(), window_icon: Value::Constant(None), modifiers: Dynamic::default(), enabled_buttons: Value::dynamic(WindowButtons::all()), fullscreen: Value::default(), shortcuts: Value::default(), on_init: None, on_file_drop: None, }, ); CushyWindow { window, kludgine } } /// Returns an initialized [`VirtualWindow`]. #[must_use] pub fn finish_virtual(self, device: &wgpu::Device, queue: &wgpu::Queue) -> VirtualWindow { let mut state = VirtualState::new(); state.size = self.initial_size; let mut cushy = self.finish(&mut state, device, queue); cushy.set_focused(true); VirtualWindow { cushy, state, last_rendered_at: None, } } } /// A standalone Cushy window. /// /// This type allows rendering Cushy applications directly into any wgpu /// application. pub struct CushyWindow { window: OpenWindow, kludgine: Kludgine, } impl CushyWindow { /// Prepares all necessary resources and operations necessary to render the /// next frame. pub fn prepare(&mut self, window: W, device: &wgpu::Device, queue: &wgpu::Queue) where W: PlatformWindowImplementation, { self.window.prepare( window, &mut kludgine::Graphics::new(&mut self.kludgine, device, queue), ); } /// Renders this window in a wgpu render pass created from `pass`. /// /// Returns the submission index of the last command submission, if any /// commands were submitted. pub fn render( &mut self, pass: &wgpu::RenderPassDescriptor<'_>, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { self.render_with(pass, device, queue, None) } /// Renders this window in a wgpu render pass created from `pass`. /// /// Returns the submission index of the last command submission, if any /// commands were submitted. pub fn render_with( &mut self, pass: &wgpu::RenderPassDescriptor<'_>, device: &wgpu::Device, queue: &wgpu::Queue, additional_drawing: Option<&Drawing>, ) -> Option { let mut frame = self.kludgine.next_frame(); let mut gfx = frame.render(pass, device, queue); self.window.contents.render(1., &mut gfx); if let Some(additional) = additional_drawing { additional.render(1., &mut gfx); } drop(gfx); frame.submit(queue) } /// Renders this window into `texture` after performing `load_op`. pub fn render_into( &mut self, texture: &kludgine::Texture, load_op: wgpu::LoadOp, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { let mut frame = self.kludgine.next_frame(); let mut gfx = frame.render_into(texture, load_op, device, queue); self.window.contents.render(1., &mut gfx); drop(gfx); frame.submit(queue) } /// Returns a new [`kludgine::Graphics`] context for this window. #[must_use] pub fn graphics<'gfx>( &'gfx mut self, device: &'gfx wgpu::Device, queue: &'gfx wgpu::Queue, ) -> kludgine::Graphics<'gfx> { kludgine::Graphics::new(&mut self.kludgine, device, queue) } /// Sets the window's focused status. /// /// Being focused means that the window is expecting to be able to receive /// user input. pub fn set_focused(&mut self, focused: bool) { self.window.set_focused(focused); } /// Sets the window's occlusion status. /// /// This should only be set to true if the window is not visible at all to /// the end user due to being offscreen, minimized, or fully hidden behind /// other windows. pub fn set_occluded(&mut self, window: &W, occluded: bool) where W: PlatformWindowImplementation, { self.window.set_occluded(window, occluded); } /// Requests that the window close. /// /// Returns true if the request should be honored. pub fn request_close(&mut self, window: W) -> bool where W: PlatformWindowImplementation, { self.window.close_requested(window, &mut self.kludgine) } /// Returns the current size of the window. pub const fn size(&self) -> Size { self.kludgine.size() } /// Returns the current DPI scale of the window. pub const fn dpi_scale(&self) -> Fraction { self.kludgine.dpi_scale() } /// Returns the effective scale of the window. pub fn effective_scale(&self) -> Fraction { self.kludgine.scale() } /// Updates the dimensions and DPI scaling of the window. pub fn resize( &mut self, window: &W, new_size: Size, new_scale: impl Into, new_zoom: impl Into, queue: &wgpu::Queue, ) where W: PlatformWindowImplementation, { self.kludgine.resize(new_size, new_scale, new_zoom, queue); self.window.resized(new_size, window); } /// Sets the window's position. pub fn set_position(&mut self, new_position: Point) { self.window.moved(new_position, new_position); } /// Provide keyboard input to this virtual window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn keyboard_input( &mut self, window: W, device_id: DeviceId, input: KeyEvent, is_synthetic: bool, ) -> EventHandling where W: PlatformWindowImplementation, { self.window .keyboard_input(window, &mut self.kludgine, device_id, input, is_synthetic) } /// Provides mouse wheel input to this window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn mouse_wheel( &mut self, window: W, device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, ) -> EventHandling where W: PlatformWindowImplementation, { self.window .mouse_wheel(window, &mut self.kludgine, device_id, delta, phase) } /// Provides input manager events to this window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn ime(&mut self, window: W, ime: &Ime) -> EventHandling where W: PlatformWindowImplementation, { self.window.ime(window, &mut self.kludgine, ime) } /// Provides cursor movement events to this window. pub fn cursor_moved( &mut self, window: W, device_id: DeviceId, position: impl Into>, ) where W: PlatformWindowImplementation, { self.window .cursor_moved(window, &mut self.kludgine, device_id, position); } /// Notifies the window that the cursor is no longer within the window. pub fn cursor_left(&mut self, window: W) where W: PlatformWindowImplementation, { self.window.cursor_left(window, &mut self.kludgine); } /// Provides mouse input events to tihs window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn mouse_input( &mut self, window: W, device_id: DeviceId, state: ElementState, button: MouseButton, ) -> EventHandling where W: PlatformWindowImplementation, { self.window .mouse_input(window, &mut self.kludgine, device_id, state, button) } } /// A virtual Cushy window. /// /// This type allows rendering Cushy applications directly into any wgpu /// application. pub struct VirtualWindow { cushy: CushyWindow, state: VirtualState, last_rendered_at: Option, } impl VirtualWindow { /// Prepares all necessary resources and operations necessary to render the /// next frame. /// /// # Errors /// /// If during the preparation of rendering, the window is resized, /// `Err(Resized)` is returned and Cushy will immediately resize the /// graphics context and begin rendering again. pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) { let now = Instant::now(); self.state.elapsed = self .last_rendered_at .map(|i| now.duration_since(i)) .unwrap_or_default(); self.last_rendered_at = Some(now); self.state.dynamic.redraw_target.set(RedrawTarget::Never); self.cushy.prepare(&mut self.state, device, queue); } /// Renders this window in a wgpu render pass created from `pass`. /// /// Returns the submission index of the last command submission, if any /// commands were submitted. pub fn render( &mut self, pass: &wgpu::RenderPassDescriptor<'_>, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { self.render_with(pass, device, queue, None) } /// Renders this window in a wgpu render pass created from `pass`. /// /// Returns the submission index of the last command submission, if any /// commands were submitted. pub fn render_with( &mut self, pass: &wgpu::RenderPassDescriptor<'_>, device: &wgpu::Device, queue: &wgpu::Queue, additional_drawing: Option<&Drawing>, ) -> Option { self.cushy .render_with(pass, device, queue, additional_drawing) } /// Renders this window into `texture` after performing `load_op`. pub fn render_into( &mut self, texture: &kludgine::Texture, load_op: wgpu::LoadOp, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { self.cushy.render_into(texture, load_op, device, queue) } /// Returns a new [`kludgine::Graphics`] context for this window. #[must_use] pub fn graphics<'gfx>( &'gfx mut self, device: &'gfx wgpu::Device, queue: &'gfx wgpu::Queue, ) -> kludgine::Graphics<'gfx> { self.cushy.graphics(device, queue) } /// Requests that the window close. /// /// Returns true if the request should be honored. pub fn request_close(&mut self) -> bool { if self.cushy.request_close(&mut self.state) { self.state.closed = true; true } else { self.state.dynamic.close_requested.set(false); false } } /// Sets the window's focused status. /// /// Being focused means that the window is expecting to be able to receive /// user input. pub fn set_focused(&mut self, focused: bool) { self.cushy.set_focused(focused); } /// Sets the window's occlusion status. /// /// This should only be set to true if the window is not visible at all to /// the end user due to being offscreen, minimized, or fully hidden behind /// other windows. pub fn set_occluded(&mut self, occluded: bool) { self.cushy.set_occluded(&&mut self.state, occluded); } /// Returns true if this window should no longer be open. #[must_use] pub fn closed(&self) -> bool { self.state.closed } /// Returns a reference to the window's state. #[must_use] pub const fn state(&self) -> &VirtualState { &self.state } /// Returns the current size of the window. pub const fn size(&self) -> Size { self.cushy.size() } /// Returns the current DPI scale of the window. pub const fn dpi_scale(&self) -> Fraction { self.cushy.dpi_scale() } /// Updates the dimensions and DPI scaling of the window. pub fn resize( &mut self, new_size: Size, new_scale: impl Into, queue: &wgpu::Queue, ) { self.cushy.resize( &&mut self.state, new_size, new_scale, self.cushy.kludgine.zoom(), queue, ); } /// Sets the window's position. pub fn set_position(&mut self, new_position: Point) { self.cushy.set_position(new_position); } /// Provide keyboard input to this virtual window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn keyboard_input( &mut self, device_id: DeviceId, input: KeyEvent, is_synthetic: bool, ) -> EventHandling { self.cushy .keyboard_input(&mut self.state, device_id, input, is_synthetic) } /// Provides mouse wheel input to this window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn mouse_wheel( &mut self, device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, ) -> EventHandling { self.cushy .mouse_wheel(&mut self.state, device_id, delta, phase) } /// Provides input manager events to this window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn ime(&mut self, ime: &Ime) -> EventHandling { self.cushy.ime(&mut self.state, ime) } /// Provides cursor movement events to this window. pub fn cursor_moved(&mut self, device_id: DeviceId, position: impl Into>) { self.cushy .cursor_moved(&mut self.state, device_id, position); } /// Notifies the window that the cursor is no longer within the window. pub fn cursor_left(&mut self) { self.cushy.cursor_left(&mut self.state); } /// Provides mouse input events to tihs window. /// /// Returns whether the event was [`HANDLED`] or [`IGNORED`]. pub fn mouse_input( &mut self, device_id: DeviceId, state: ElementState, button: MouseButton, ) -> EventHandling { self.cushy .mouse_input(&mut self.state, device_id, state, button) } } /// A color format containing 8-bit red, green, and blue channels. pub struct Rgb8; /// A color format containing 8-bit red, green, blue, and alpha channels. pub struct Rgba8; /// A format that can be captured in a [`VirtualRecorder`]. pub trait CaptureFormat: sealed::CaptureFormat {} impl CaptureFormat for Rgb8 {} impl sealed::CaptureFormat for Rgb8 { const HAS_ALPHA: bool = false; fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32) { let packed_width = width * 4; // Tightly pack the rgb data, discarding the alpha and extra padding.q let mut index = 0; data.retain(|_| { let retain = index % bytes_per_row < packed_width && index % 4 < 3; index += 1; retain }); } fn load_image(data: &[u8], size: Size) -> DynamicImage { DynamicImage::ImageRgb8( RgbImage::from_vec(size.width.get(), size.height.get(), data.to_vec()) .expect("incorrect dimensions"), ) } fn pixel_color(location: Point, data: &[u8], size: Size) -> Color { let pixel_offset = pixel_offset(data, location, size, 3); Color::new(pixel_offset[0], pixel_offset[1], pixel_offset[2], 255) } } fn pixel_offset( data: &[u8], location: Point, size: Size, bytes_per_component: u32, ) -> &[u8] { assert!(location.x < size.width && location.y < size.height); let width = size.width.get(); let index = location.y.get() * width + location.x.get(); &data[usize::try_from(index * bytes_per_component).expect("offset out of bounds")..] } impl CaptureFormat for Rgba8 {} impl sealed::CaptureFormat for Rgba8 { const HAS_ALPHA: bool = true; fn convert_rgba(data: &mut Vec, width: u32, bytes_per_row: u32) { let packed_width = width * 4; if packed_width != bytes_per_row { // Tightly pack the rgba data let mut index = 0; data.retain(|_| { let retain = index % bytes_per_row < packed_width; index += 1; retain }); } } fn load_image(data: &[u8], size: Size) -> DynamicImage { DynamicImage::ImageRgba8( RgbaImage::from_vec(size.width.get(), size.height.get(), data.to_vec()) .expect("incorrect dimensions"), ) } fn pixel_color(location: Point, data: &[u8], size: Size) -> Color { let pixel_offset = pixel_offset(data, location, size, 4); Color::new( pixel_offset[0], pixel_offset[1], pixel_offset[2], pixel_offset[3], ) } } /// A builder of a [`VirtualRecorder`]. pub struct VirtualRecorderBuilder { contents: WidgetInstance, size: Size, scale: f32, format: PhantomData, resize_to_fit: bool, } impl VirtualRecorderBuilder { /// Returns a builder of a [`VirtualRecorder`] that renders `contents`. pub fn new(contents: impl MakeWidget) -> Self { Self { contents: contents.make_widget(), size: Size::upx(800, 600), scale: 1.0, format: PhantomData, resize_to_fit: false, } } /// Enables transparency support to render the contents without a background /// color. #[must_use] pub fn with_alpha(self) -> VirtualRecorderBuilder { VirtualRecorderBuilder { contents: self.contents, size: self.size, scale: self.scale, resize_to_fit: self.resize_to_fit, format: PhantomData, } } } impl VirtualRecorderBuilder where Format: CaptureFormat, { /// Sets the size of the virtual window. #[must_use] pub fn size(mut self, size: Size) -> Self where Unit: Into, { self.size = size.map(Into::into); self } /// Sets the DPI scaling to apply to this virtual window. /// /// When scale is 1.0, resolution-independent content will be rendered at /// 96-ppi. /// /// This setting does not affect the image's pixel dimensions. #[must_use] pub fn scale(mut self, scale: f32) -> Self { self.scale = scale; self } /// Sets this virtual recorder to allow updating its size based on the /// contents being rendered. #[must_use] pub fn resize_to_fit(mut self) -> Self { self.resize_to_fit = true; self } /// Returns an initialized [`VirtualRecorder`]. pub fn finish(self) -> Result, VirtualRecorderError> { VirtualRecorder::new(self.size, self.scale, self.resize_to_fit, self.contents) } } struct Capture { bytes: u64, bytes_per_row: u32, buffer: wgpu::Buffer, texture: Texture, multisample: Texture, } impl Capture { fn map_into( &self, buffer: &mut Vec, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Result<(), wgpu::BufferAsyncError> where Format: CaptureFormat, { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); self.texture.copy_to_buffer( wgpu::ImageCopyBuffer { buffer: &self.buffer, layout: wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(self.bytes_per_row), rows_per_image: None, }, }, &mut encoder, ); queue.submit([encoder.finish()]); let map_result = Arc::new(Mutex::new(None)); let slice = self.buffer.slice(0..self.bytes); slice.map_async(wgpu::MapMode::Read, { let map_result = map_result.clone(); move |result| { *map_result.lock() = Some(result); } }); buffer.clear(); buffer.reserve(self.bytes.cast()); loop { device.poll(wgpu::Maintain::Poll); let mut result = map_result.lock(); if let Some(result) = result.take() { result?; break; } } buffer.extend_from_slice(&slice.get_mapped_range()); self.buffer.unmap(); Format::convert_rgba(buffer, self.texture.size().width.get(), self.bytes_per_row); Ok(()) } } /// A recorder of a [`VirtualWindow`]. pub struct VirtualRecorder { /// The virtual window being recorded. pub window: VirtualWindow, device: Arc, queue: Arc, capture: Option>, data: Vec, data_size: Size, cursor: Dynamic>, cursor_visible: bool, cursor_graphic: Drawing, format: PhantomData, } impl VirtualRecorder where Format: CaptureFormat, { /// Returns a new virtual recorder that renders `contents` into a graphic of /// `size`. /// /// `scale` adjusts the default DPI scaling to perform. It does not affect /// the `size`. pub fn new( size: Size, scale: f32, resize_to_fit: bool, contents: impl MakeWidget, ) -> Result { let wgpu = wgpu::Instance::default(); let adapter = pollster::block_on(wgpu.request_adapter(&wgpu::RequestAdapterOptions::default())) .ok_or(VirtualRecorderError::NoAdapter)?; let (device, queue) = pollster::block_on(adapter.request_device( &wgpu::DeviceDescriptor { label: None, required_features: Kludgine::REQURED_FEATURES, required_limits: Kludgine::adjust_limits(wgpu::Limits::downlevel_webgl2_defaults()), memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, ))?; let window = contents .build_standalone_window() .size(size) .scale(scale) .transparent() .resize_to_fit(resize_to_fit) .finish_virtual(&device, &queue); let mut recorder = Self { window, device: Arc::new(device), queue: Arc::new(queue), cursor: Dynamic::default(), cursor_graphic: Drawing::default(), cursor_visible: false, capture: None, data: Vec::new(), data_size: Size::ZERO, format: PhantomData, }; recorder.refresh()?; if resize_to_fit && recorder.window.state.size != recorder.window.size() { recorder.refresh()?; } Ok(recorder) } /// Returns the tightly-packed captured bytes. /// /// The layout of this data is determined by the `Format` generic. pub fn bytes(&self) -> &[u8] { &self.data } /// Returns the color of the pixel at `location`. /// /// # Panics /// /// This function will panic if location is outside of the bounds of the /// captured image. When the window's size has been changed, this function /// operates on the size of the window when the last call to /// [`Self::refresh()`] was made. pub fn pixel_color(&self, location: Point) -> Color where Unit: Into, { Format::pixel_color(location.map(Into::into), self.bytes(), self.data_size) } /// Asserts that the color of the pixel at `location` is `expected`. /// /// This function allows for slight color variations. This is because of how /// colorspace corrections can lead to rounding errors. /// /// # Panics /// /// This function panics if the color is not the expected color. #[track_caller] pub fn assert_pixel_color(&self, location: Point, expected: Color, component: &str) where Unit: Into, { let location = location.map(Into::into); let color = self.pixel_color(location); let max_delta = color .red() .abs_diff(expected.red()) .max(color.green().abs_diff(expected.green())) .max(color.blue().abs_diff(expected.blue())) .max(color.alpha().abs_diff(expected.alpha())); assert!( max_delta <= 1, "assertion failed: {component} at {location:?} was {color:?}, not {expected:?}" ); } /// Returns the current contents as an image. pub fn image(&self) -> DynamicImage { Format::load_image(self.bytes(), self.data_size) } fn recreate_buffers_if_needed(&mut self, size: Size, bytes: u64, bytes_per_row: u32) { if self .capture .as_ref() .map_or(true, |capture| capture.texture.size() != size) { let texture = Texture::new( &self.window.graphics(&self.device, &self.queue), size, wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, wgpu::FilterMode::Linear, ); let multisample = Texture::multisampled( &self.window.graphics(&self.device, &self.queue), 4, size, wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, wgpu::FilterMode::Linear, ); let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: bytes, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); self.capture = Some(Box::new(Capture { bytes, bytes_per_row, buffer, texture, multisample, })); } } fn redraw(&mut self) { let mut render_size = self.window.size().ceil(); if self.window.state.size != render_size { let current_scale = self.window.dpi_scale(); self.window .resize(self.window.state.size, current_scale, &self.queue); render_size = self.window.state.size; } let bytes_per_row = copy_buffer_aligned_bytes_per_row(render_size.width.get() * 4); let size = u64::from(bytes_per_row) * u64::from(render_size.height.get()); self.recreate_buffers_if_needed(render_size, size, bytes_per_row); let capture = self.capture.as_ref().assert("always initialized above"); if self.cursor_visible { let mut gfx = self.window.graphics(&self.device, &self.queue); let mut frame = self.cursor_graphic.new_frame(&mut gfx); frame.draw_shape( Shape::filled_circle(Px::new(4), Color::WHITE, Origin::Center) .translate_by(self.cursor.get()), ); drop(frame); } self.window.prepare(&self.device, &self.queue); self.window.render_with( &wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: capture.multisample.view(), resolve_target: Some(capture.texture.view()), ops: wgpu::Operations { load: wgpu::LoadOp::Clear(Color::CLEAR_BLACK.into()), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }, &self.device, &self.queue, self.cursor_visible.then_some(&self.cursor_graphic), ); } /// Redraws the contents. pub fn refresh(&mut self) -> Result<(), wgpu::BufferAsyncError> { self.redraw(); let capture = self.capture.as_ref().assert("always initialized above"); capture.map_into::(&mut self.data, &self.device, &self.queue)?; self.data_size = capture.texture.size(); Ok(()) } /// Sets the cursor position immediately. pub fn set_cursor_position(&self, position: Point) { self.cursor.set(position); } /// Enables or disables drawing of the virtual cursor. pub fn set_cursor_visible(&mut self, visible: bool) { self.cursor_visible = visible; } /// Begins recording an animated png. pub fn record_animated_png(&mut self, target_fps: u8) -> AnimationRecorder<'_, Format> { AnimationRecorder { target_fps, assembler: Some(FrameAssembler::spawn::( self.device.clone(), self.queue.clone(), )), recorder: self, } } /// Returns a recorder that does not store any rendered frames. pub fn simulate_animation(&mut self) -> AnimationRecorder<'_, Format> { AnimationRecorder { target_fps: 0, assembler: None, recorder: self, } } } fn copy_buffer_aligned_bytes_per_row(width: u32) -> u32 { (width + COPY_BYTES_PER_ROW_ALIGNMENT - 1) / COPY_BYTES_PER_ROW_ALIGNMENT * COPY_BYTES_PER_ROW_ALIGNMENT } /// An animated PNG recorder. pub struct AnimationRecorder<'a, Format> { recorder: &'a mut VirtualRecorder, target_fps: u8, assembler: Option, } impl AnimationRecorder<'_, Format> where Format: CaptureFormat, { /// Animates the cursor to move from its current location to `location`. pub fn animate_cursor_to( &mut self, location: Point, over: Duration, easing: impl Easing, ) -> Result<(), VirtualRecorderError> { self.recorder .cursor .transition_to(location) .over(over) .with_easing(easing) .launch(); self.wait_for(over) } /// Animates pressing and releasing a mouse button at the current cursor /// location. pub fn animate_mouse_button( &mut self, button: MouseButton, duration: Duration, ) -> Result<(), VirtualRecorderError> { let _ = self.recorder .window .mouse_input(DeviceId::Virtual(0), ElementState::Pressed, button); self.wait_for(duration)?; let _ = self.recorder .window .mouse_input(DeviceId::Virtual(0), ElementState::Released, button); Ok(()) } /// Simulates a key down and key up event with the given information. pub fn animate_keypress( &mut self, physical_key: PhysicalKey, logical_key: Key, text: Option<&str>, duration: Duration, ) -> Result<(), VirtualRecorderError> { let text = text.map(SmolStr::new); let half_duration = duration / 2; let mut event = KeyEvent { physical_key, logical_key, text, state: ElementState::Pressed, repeat: false, location: KeyLocation::Standard, modifiers: Modifiers::default(), }; self.recorder .window .keyboard_input(DeviceId::Virtual(0), event.clone(), true); self.wait_for(half_duration)?; event.state = ElementState::Released; self.recorder .window .keyboard_input(DeviceId::Virtual(0), event.clone(), true); self.wait_for(half_duration) } /// Animates entering the graphemes from `text` over `duration`. pub fn animate_text_input( &mut self, text: &str, duration: Duration, ) -> Result<(), VirtualRecorderError> { let graphemes = text.graphemes(true).count(); let delay_per_event = Duration::from_nanos(duration.as_nanos().cast::() / graphemes.cast::() / 2); for grapheme in text.graphemes(true) { let grapheme = SmolStr::new(grapheme); let mut event = KeyEvent { physical_key: PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), logical_key: Key::Character(grapheme.clone()), text: Some(SmolStr::new(grapheme)), location: KeyLocation::Standard, state: ElementState::Pressed, repeat: false, modifiers: Modifiers::default(), }; let _handled = self.recorder .window .keyboard_input(DeviceId::Virtual(0), event.clone(), true); self.wait_for(delay_per_event)?; event.state = ElementState::Released; let _handled = self .recorder .window .keyboard_input(DeviceId::Virtual(0), event, true); self.wait_for(delay_per_event)?; } Ok(()) } /// Waits for `duration`, rendering frames as needed. pub fn wait_for(&mut self, duration: Duration) -> Result<(), VirtualRecorderError> { self.wait_until(Instant::now() + duration) } /// Waits until `time`, rendering frames as needed. pub fn wait_until(&mut self, time: Instant) -> Result<(), VirtualRecorderError> { let Some(assembler) = self.assembler.as_ref() else { return Ok(()); }; let frame_duration = Duration::from_micros(1_000_000 / u64::from(self.target_fps)); let mut last_frame = Instant::now(); loop { let now = Instant::now(); let final_frame = now > time; self.recorder .window .cursor_moved(DeviceId::Virtual(0), self.recorder.cursor.get()); let next_frame = match self.recorder.window.state.dynamic.redraw_target.get() { RedrawTarget::Never => now + frame_duration, RedrawTarget::Now => now, RedrawTarget::At(instant) => now.min(instant), }; if final_frame || next_frame <= now { // Try to reuse an existing capture instead of forcing an // allocation. if let Ok(capture) = assembler.resuable_captures.try_recv() { self.recorder.capture = Some(capture); } let elapsed = now.saturating_duration_since(last_frame); last_frame = now; self.recorder.redraw(); let capture = self.recorder.capture.take().assert("always present"); if assembler.sender.send((capture, elapsed)).is_err() { break; } } if final_frame { break; } let render_duration = now.elapsed(); std::thread::sleep(frame_duration.saturating_sub(render_duration)); } Ok(()) } /// Encodes the currently recorded frames into a new file at `path`. /// /// If this animation was created from /// [`VirtualRecorder::simulate_animation`], this function will do nothing. pub fn write_to(self, path: impl AsRef) -> Result<(), VirtualRecorderError> { let Some(frames) = self.assembler.map(FrameAssembler::finish).transpose()? else { return Ok(()); }; let mut file = std::fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(path)?; let mut encoder = png::Encoder::new( &mut file, self.recorder.window.size().width.get(), self.recorder.window.size().height.get(), ); encoder.set_color(if Format::HAS_ALPHA { png::ColorType::Rgba } else { png::ColorType::Rgb }); encoder.set_adaptive_filter(png::AdaptiveFilterType::Adaptive); encoder.set_animated(u32::try_from(frames.len()).assert("too many frames"), 0)?; encoder.set_compression(png::Compression::Best); let mut current_frame_delay = Duration::ZERO; let mut writer = encoder.write_header()?; for frame in &frames { if current_frame_delay != frame.duration && frames.len() > 1 { current_frame_delay = frame.duration; // This has a limitation that a single frame can't be longer // than ~6.5 seconds, but it ensures frame timing is more // accurate. writer.set_frame_delay( u16::try_from(current_frame_delay.as_nanos() / 100_000).unwrap_or(u16::MAX), 10_000, )?; } writer.write_image_data(&frame.data)?; } writer.finish()?; file.sync_all()?; Ok(()) } } struct Frame { data: Vec, duration: Duration, } /// An error from a [`VirtualRecorder`]. #[derive(Debug)] pub enum VirtualRecorderError { /// No compatible wgpu adapters could be found. NoAdapter, /// An error occurred requesting a device. RequestDevice(wgpu::RequestDeviceError), /// The capture texture dimensions are too large to fit in the current host /// platform's memory. TooLarge, /// An error occurred trying to read a buffer. MapBuffer(wgpu::BufferAsyncError), /// An error occurred encoding a png image. PngEncode(png::EncodingError), } impl From for VirtualRecorderError { fn from(value: png::EncodingError) -> Self { Self::PngEncode(value) } } impl From for VirtualRecorderError { fn from(value: wgpu::RequestDeviceError) -> Self { Self::RequestDevice(value) } } impl From for VirtualRecorderError { fn from(value: wgpu::BufferAsyncError) -> Self { Self::MapBuffer(value) } } impl From for VirtualRecorderError { fn from(_: TryFromIntError) -> Self { Self::TooLarge } } impl From for VirtualRecorderError { fn from(value: io::Error) -> Self { Self::PngEncode(value.into()) } } impl std::fmt::Display for VirtualRecorderError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VirtualRecorderError::NoAdapter => { f.write_str("no compatible graphics adapters were found") } VirtualRecorderError::RequestDevice(err) => { write!(f, "error requesting graphics device: {err}") } VirtualRecorderError::TooLarge => { f.write_str("the rendered surface is too large for this cpu architecture") } VirtualRecorderError::MapBuffer(err) => { write!(f, "error reading rendered graphics data: {err}") } VirtualRecorderError::PngEncode(err) => write!(f, "error encoding png: {err}"), } } } impl std::error::Error for VirtualRecorderError {} /// A unique identifier of an input device. #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum DeviceId { /// A winit-supplied device id. Winit(winit::event::DeviceId), /// A simulated device. Virtual(u64), } impl From for DeviceId { fn from(value: winit::event::DeviceId) -> Self { Self::Winit(value) } } struct FrameAssembler { sender: mpsc::SyncSender<(Box, Duration)>, result: mpsc::Receiver, VirtualRecorderError>>, resuable_captures: mpsc::Receiver>, } impl FrameAssembler { fn spawn(device: Arc, queue: Arc) -> Self where Format: CaptureFormat, { let (frame_sender, frame_receiver) = mpsc::sync_channel(1000); let (finished_frame_sender, finished_frame_receiver) = mpsc::sync_channel(600); let (result_sender, result_receiver) = mpsc::sync_channel(1); std::thread::spawn(move || { Self::assembler_thread::( &frame_receiver, &result_sender, &finished_frame_sender, &device, &queue, ); }); Self { sender: frame_sender, result: result_receiver, resuable_captures: finished_frame_receiver, } } fn finish(self) -> Result, VirtualRecorderError> { drop(self.sender); self.result.recv().assert("thread panicked") } fn assembler_thread( frames: &mpsc::Receiver<(Box, Duration)>, result: &mpsc::SyncSender, VirtualRecorderError>>, reusable: &mpsc::SyncSender>, device: &wgpu::Device, queue: &wgpu::Queue, ) where Format: CaptureFormat, { let mut assembled = Vec::::new(); let mut buffer = Vec::new(); while let Ok((capture, elapsed)) = frames.recv() { if let Err(err) = capture.map_into::(&mut buffer, device, queue) { let _result = result.send(Err(err.into())); return; } match assembled.last_mut() { Some(frame) if frame.data == buffer => { frame.duration += elapsed; } _ => { assembled.push(Frame { data: std::mem::take(&mut buffer), duration: elapsed, }); } } let _result = reusable.try_send(capture); } let _result = result.send(Ok(assembled)); } } /// Describes a keyboard input targeting a window. #[derive(Debug, Clone, Eq, PartialEq)] pub struct KeyEvent { /// The logical key that is interpretted from the `physical_key`. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::logical_key) for /// more information. pub logical_key: Key, /// The physical key that caused this event. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::physical_key) for /// more information. pub physical_key: PhysicalKey, /// The text being input by this event, if any. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::text) for /// more information. pub text: Option, /// The physical location of the key being presed. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::location) for /// more information. pub location: KeyLocation, /// The state of this key for this event. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::state) for /// more information. pub state: ElementState, /// If true, this event was caused by a key being repeated. /// /// See [`KeyEvent::logical_key`](winit::event::KeyEvent::logical_key) for /// more information. pub repeat: bool, /// The modifiers state active for this event. pub modifiers: Modifiers, } impl KeyEvent { /// Returns a new key event from a winit key event and modifiers. #[must_use] pub fn from_winit(event: winit::event::KeyEvent, modifiers: Modifiers) -> Self { Self { physical_key: event.physical_key, logical_key: event.logical_key, text: event.text, location: event.location, state: event.state, repeat: event.repeat, modifiers, } } }