#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
use std::cell::Cell;
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
use std::collections::HashMap;
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
use std::hash::Hash;
use std::{cell::RefCell, fmt, rc::Rc};
use cranpose_core::MutableState;
use cranpose_ui::{Modifier, Point, PointerEventKind, PointerInputScope, Size, composable};
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct WindowId(u64);
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
impl WindowId {
#[cfg(test)]
pub(crate) fn from_static(id: &'static str) -> Self {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
Self(hasher.finish())
}
pub(crate) fn raw(self) -> u64 {
self.0
}
pub(crate) fn from_node(node: cranpose_core::NodeId) -> Self {
Self(node as u64)
}
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) type NativeWindowKey = WindowId;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NativeWindowPositionOrigin {
Screen,
HostWindow,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum WindowFocus {
Never,
#[default]
WhenNoneFocused,
Always,
}
#[derive(Clone, Debug, PartialEq)]
pub struct NativeWindowOptions {
pub title: String,
pub width: f32,
pub height: f32,
pub x: Option<f32>,
pub y: Option<f32>,
pub position_origin: NativeWindowPositionOrigin,
pub decorations: bool,
pub transparent: bool,
pub shadow: bool,
pub resizable: bool,
pub visible: bool,
pub always_on_top: bool,
pub min_width: Option<f32>,
pub min_height: Option<f32>,
pub max_width: Option<f32>,
pub max_height: Option<f32>,
pub focus: WindowFocus,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
#[derive(Clone)]
pub(crate) struct NativeWindowParts {
pub(crate) options: NativeWindowOptions,
pub(crate) events: NativeWindowEvents,
pub(crate) state: Option<WindowState>,
}
impl NativeWindowOptions {
pub fn new(title: impl Into<String>, width: f32, height: f32) -> Self {
Self {
title: title.into(),
width,
height,
x: None,
y: None,
position_origin: NativeWindowPositionOrigin::Screen,
decorations: true,
transparent: false,
shadow: true,
resizable: true,
visible: true,
always_on_top: false,
min_width: None,
min_height: None,
max_width: None,
max_height: None,
focus: WindowFocus::default(),
}
}
pub fn borderless(title: impl Into<String>, width: f32, height: f32) -> Self {
Self {
decorations: false,
resizable: false,
..Self::new(title, width, height)
}
}
pub fn with_position(mut self, x: f32, y: f32) -> Self {
self.x = Some(x);
self.y = Some(y);
self.position_origin = NativeWindowPositionOrigin::Screen;
self
}
pub fn with_host_window_position(mut self, x: f32, y: f32) -> Self {
self.x = Some(x);
self.y = Some(y);
self.position_origin = NativeWindowPositionOrigin::HostWindow;
self
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.transparent = transparent;
self
}
pub fn with_shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
pub fn with_resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn with_always_on_top(mut self, always_on_top: bool) -> Self {
self.always_on_top = always_on_top;
self
}
pub fn with_focus(mut self, focus: WindowFocus) -> Self {
self.focus = focus;
self
}
pub fn with_min_size(mut self, width: f32, height: f32) -> Self {
self.min_width = Some(width);
self.min_height = Some(height);
self
}
pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
self.max_width = Some(width);
self.max_height = Some(height);
self
}
}
#[derive(Clone, Default)]
pub(crate) struct NativeWindowEvents {
pub(crate) on_moved: Option<Rc<dyn Fn(f32, f32)>>,
pub(crate) on_resized: Option<Rc<dyn Fn(f32, f32)>>,
pub(crate) on_close_requested: Option<Rc<dyn Fn()>>,
}
impl NativeWindowEvents {
fn new() -> Self {
Self::default()
}
fn with_on_moved(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
let next = Rc::new(callback);
self.on_moved = Some(match self.on_moved.take() {
Some(previous) => Rc::new(move |x, y| {
previous(x, y);
next(x, y);
}),
None => next,
});
self
}
fn with_on_resized(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
let next = Rc::new(callback);
self.on_resized = Some(match self.on_resized.take() {
Some(previous) => Rc::new(move |width, height| {
previous(width, height);
next(width, height);
}),
None => next,
});
self
}
fn with_on_close_requested(mut self, callback: impl Fn() + 'static) -> Self {
let next = Rc::new(callback);
self.on_close_requested = Some(match self.on_close_requested.take() {
Some(previous) => Rc::new(move || {
previous();
next();
}),
None => next,
});
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WindowState {
position: MutableState<Option<Point>>,
size: MutableState<Size>,
frame: MutableState<Size>,
presented: MutableState<bool>,
}
impl WindowState {
pub fn frame_size(self) -> Size {
self.frame.get()
}
pub fn frame_size_non_reactive(self) -> Size {
self.frame.get_non_reactive()
}
pub fn set_frame_size(self, frame: Size) {
if self.frame.get_non_reactive() != frame {
self.frame.set(frame);
}
}
pub fn position(self) -> Option<Point> {
self.position.get()
}
pub fn position_non_reactive(self) -> Option<Point> {
self.position.get_non_reactive()
}
pub fn set_position(self, position: Option<Point>) {
if self.position.get_non_reactive() != position {
self.position.set(position);
}
}
pub fn translate(self, dx: f32, dy: f32) {
if let Some(position) = self.position_non_reactive() {
self.set_position(Some(Point::new(position.x + dx, position.y + dy)));
}
}
pub fn size(self) -> Size {
self.size.get()
}
pub fn size_non_reactive(self) -> Size {
self.size.get_non_reactive()
}
pub fn set_size(self, size: Size) {
if self.size.get_non_reactive() != size {
self.size.set(size);
}
}
pub fn presented(self) -> bool {
self.presented.get()
}
pub fn presented_non_reactive(self) -> bool {
self.presented.get_non_reactive()
}
pub fn set_presented(self, presented: bool) {
if self.presented.is_alive() && self.presented.get_non_reactive() != presented {
self.presented.set(presented);
}
}
pub fn new(width: f32, height: f32) -> Self {
Self::sized(None, Size::new(width, height))
}
pub fn placed_at(x: f32, y: f32, width: f32, height: f32) -> Self {
Self::sized(Some(Point::new(x, y)), Size::new(width, height))
}
fn sized(position: Option<Point>, size: Size) -> Self {
WindowState {
position: cranpose_core::mutableStateOf(position),
size: cranpose_core::mutableStateOf(size),
frame: cranpose_core::mutableStateOf(size),
presented: cranpose_core::mutableStateOf(false),
}
}
}
#[allow(non_snake_case)]
#[composable]
#[track_caller]
pub fn rememberWindowState(width: f32, height: f32) -> WindowState {
cranpose_core::remember(move || WindowState::new(width, height)).with(|state| *state)
}
#[allow(non_snake_case)]
#[composable]
#[track_caller]
pub fn rememberWindowStateAt(x: f32, y: f32, width: f32, height: f32) -> WindowState {
cranpose_core::remember(move || WindowState::placed_at(x, y, width, height))
.with(|state| *state)
}
#[derive(Clone)]
pub struct WindowConfig {
options: NativeWindowOptions,
callbacks: NativeWindowEvents,
state: Option<WindowState>,
}
impl PartialEq for WindowConfig {
fn eq(&self, other: &Self) -> bool {
self.options == other.options && self.state == other.state
}
}
impl fmt::Debug for WindowConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WindowConfig")
.field("options", &self.options)
.finish_non_exhaustive()
}
}
impl WindowConfig {
pub fn new(title: impl Into<String>, width: f32, height: f32) -> Self {
Self {
options: NativeWindowOptions::new(title, width, height),
callbacks: NativeWindowEvents::new(),
state: None,
}
}
pub fn new_for_state(title: impl Into<String>, state: WindowState) -> Self {
let size = state.size();
Self::new(title, size.width, size.height).with_state(state)
}
pub fn borderless(title: impl Into<String>, width: f32, height: f32) -> Self {
Self {
options: NativeWindowOptions::borderless(title, width, height),
callbacks: NativeWindowEvents::new(),
state: None,
}
}
pub fn borderless_for_state(title: impl Into<String>, state: WindowState) -> Self {
let size = state.size();
Self::borderless(title, size.width, size.height).with_state(state)
}
pub fn with_position(mut self, x: f32, y: f32) -> Self {
self.options = self.options.with_position(x, y);
self
}
pub fn with_host_window_position(mut self, x: f32, y: f32) -> Self {
self.options = self.options.with_host_window_position(x, y);
self
}
pub fn with_transparent(mut self, transparent: bool) -> Self {
self.options = self.options.with_transparent(transparent);
self
}
pub fn with_shadow(mut self, shadow: bool) -> Self {
self.options = self.options.with_shadow(shadow);
self
}
pub fn with_resizable(mut self, resizable: bool) -> Self {
self.options = self.options.with_resizable(resizable);
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.options = self.options.with_visible(visible);
self
}
pub fn with_always_on_top(mut self, always_on_top: bool) -> Self {
self.options = self.options.with_always_on_top(always_on_top);
self
}
pub fn with_focus(mut self, focus: WindowFocus) -> Self {
self.options = self.options.with_focus(focus);
self
}
pub fn with_min_size(mut self, width: f32, height: f32) -> Self {
self.options = self.options.with_min_size(width, height);
self
}
pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
self.options = self.options.with_max_size(width, height);
self
}
pub fn on_moved(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
self.callbacks = self.callbacks.with_on_moved(callback);
self
}
pub fn on_resized(mut self, callback: impl Fn(f32, f32) + 'static) -> Self {
self.callbacks = self.callbacks.with_on_resized(callback);
self
}
pub fn on_close_requested(mut self, callback: impl Fn() + 'static) -> Self {
self.callbacks = self.callbacks.with_on_close_requested(callback);
self
}
pub fn with_state(mut self, state: WindowState) -> Self {
let size = state.size();
self.options.width = size.width;
self.options.height = size.height;
if let Some(position) = state.position() {
self.options.x = Some(position.x);
self.options.y = Some(position.y);
self.options.position_origin = NativeWindowPositionOrigin::Screen;
}
self.state = Some(state);
self
}
pub(crate) fn state(&self) -> Option<WindowState> {
self.state
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn title(&self) -> &str {
&self.options.title
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn into_parts(self) -> NativeWindowParts {
NativeWindowParts {
options: self.options,
events: self.callbacks,
state: self.state,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WindowResizeDirection {
East,
North,
NorthEast,
NorthWest,
South,
SouthEast,
SouthWest,
West,
}
pub trait WindowModifierExt {
fn window(self, config: WindowConfig) -> Modifier;
fn window_drag_area(
self,
on_started: impl Fn() + 'static,
on_finished: impl Fn() + 'static,
) -> Modifier;
fn window_resize_area(self, direction: WindowResizeDirection) -> Modifier;
}
impl WindowModifierExt for Modifier {
fn window(self, config: WindowConfig) -> Modifier {
let modifier = crate::window_local::with_window_state_local(self, config.state());
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
{
crate::window_node::window(modifier, config)
}
#[cfg(not(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
)))]
{
let _ = config;
modifier
}
}
fn window_drag_area(
self,
on_started: impl Fn() + 'static,
on_finished: impl Fn() + 'static,
) -> Modifier {
let on_started: Rc<dyn Fn()> = Rc::new(on_started);
let on_finished: Rc<dyn Fn()> = Rc::new(on_finished);
self.pointer_input((), move |scope: PointerInputScope| {
let on_started = on_started.clone();
let on_finished = on_finished.clone();
async move {
scope
.await_pointer_event_scope(|await_scope| async move {
let mut dragging = false;
loop {
let event = await_scope.await_pointer_event().await;
match event.kind {
PointerEventKind::Down => {
if request_native_window_drag() {
dragging = true;
on_started();
}
}
PointerEventKind::Move => {
if dragging && event.buttons == Default::default() {
dragging = false;
on_finished();
}
}
PointerEventKind::Up | PointerEventKind::Cancel => {
if dragging {
dragging = false;
on_finished();
}
}
PointerEventKind::Scroll
| PointerEventKind::Zoom
| PointerEventKind::RotaryScrollPre
| PointerEventKind::RotaryScroll
| PointerEventKind::Enter
| PointerEventKind::Exit => {}
}
}
})
.await;
}
})
}
fn window_resize_area(self, direction: WindowResizeDirection) -> Modifier {
self.pointer_input(direction, move |scope: PointerInputScope| async move {
scope
.await_pointer_event_scope(|await_scope| async move {
loop {
let event = await_scope.await_pointer_event().await;
if event.kind == PointerEventKind::Down
&& request_native_window_resize(direction)
{
event.consume();
}
}
})
.await;
})
}
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) struct NativeWindowRoot {
size: Cell<Size>,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
impl NativeWindowRoot {
pub(crate) fn new(size: Size) -> Self {
Self {
size: Cell::new(size),
}
}
pub(crate) fn set_size(&self, size: Size) {
self.size.set(size);
}
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
impl cranpose_ui::WindowRootDescriptor for NativeWindowRoot {
fn layout_size(&self) -> Size {
self.size.get()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) type NativeWindowRootHandle = Rc<NativeWindowRoot>;
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) type NativeWindowOwner = Rc<()>;
type NativeWindowDragHandler = Rc<dyn Fn() -> bool>;
type NativeWindowResizeHandler = Rc<dyn Fn(WindowResizeDirection)>;
#[derive(Clone, Default)]
struct NativeWindowDispatchContext {
drag_handler: Option<NativeWindowDragHandler>,
resize_handler: Option<NativeWindowResizeHandler>,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
#[derive(Clone)]
pub(crate) struct NativeWindowRequest {
pub(crate) key: NativeWindowKey,
pub(crate) options: NativeWindowOptions,
pub(crate) events: NativeWindowEvents,
pub(crate) state: Option<WindowState>,
pub(crate) root: NativeWindowRootHandle,
pub(crate) revision: u64,
owner: NativeWindowOwner,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
struct NativeWindowRegistration {
key: NativeWindowKey,
options: NativeWindowOptions,
events: NativeWindowEvents,
state: Option<WindowState>,
root: NativeWindowRootHandle,
owner: NativeWindowOwner,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
#[derive(Default)]
pub(crate) struct NativeWindowRegistry {
windows: RefCell<HashMap<NativeWindowKey, NativeWindowRequest>>,
next_revision: Cell<u64>,
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
impl NativeWindowRegistry {
fn requests(&self) -> Vec<NativeWindowRequest> {
self.windows.borrow().values().cloned().collect()
}
fn has_requests(&self) -> bool {
!self.windows.borrow().is_empty()
}
#[cfg(test)]
fn clear(&self) {
self.windows.borrow_mut().clear();
}
fn register(&self, registration: NativeWindowRegistration) {
let revision = self.next_revision();
let key = registration.key;
self.windows.borrow_mut().insert(
key,
NativeWindowRequest {
key,
options: registration.options,
events: registration.events,
state: registration.state,
root: registration.root,
revision,
owner: registration.owner,
},
);
}
fn unregister(&self, key: NativeWindowKey, owner: NativeWindowOwner) {
let mut windows = self.windows.borrow_mut();
if windows
.get(&key)
.is_some_and(|request| Rc::ptr_eq(&request.owner, &owner))
{
windows.remove(&key);
}
}
fn next_revision(&self) -> u64 {
let current = self.next_revision.get().max(1);
self.next_revision.set(current.wrapping_add(1).max(1));
current
}
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
thread_local! {
static CURRENT_NATIVE_WINDOW_REGISTRY: crate::scoped_weak_stack::ScopedWeakStack<NativeWindowRegistry> =
const { crate::scoped_weak_stack::ScopedWeakStack::new() };
}
thread_local! {
static CURRENT_NATIVE_WINDOW_DISPATCH: RefCell<Vec<NativeWindowDispatchContext>> = const { RefCell::new(Vec::new()) };
}
fn request_native_window_drag() -> bool {
current_native_window_dispatch_context()
.and_then(|context| context.drag_handler)
.is_some_and(|handler| handler())
}
fn request_native_window_resize(direction: WindowResizeDirection) -> bool {
current_native_window_dispatch_context()
.and_then(|context| context.resize_handler)
.is_some_and(|handler| {
handler(direction);
true
})
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn with_native_window_registry<R>(
registry: &Rc<NativeWindowRegistry>,
f: impl FnOnce() -> R,
) -> R {
CURRENT_NATIVE_WINDOW_REGISTRY.with(|stack| stack.with_scope(registry, f))
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
fn current_native_window_registry() -> Option<Rc<NativeWindowRegistry>> {
CURRENT_NATIVE_WINDOW_REGISTRY.with(crate::scoped_weak_stack::ScopedWeakStack::current)
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn native_window_requests(registry: &NativeWindowRegistry) -> Vec<NativeWindowRequest> {
registry.requests()
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn has_native_window_requests(registry: &NativeWindowRegistry) -> bool {
registry.has_requests()
}
#[cfg(all(
test,
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn clear_native_window_requests(registry: &NativeWindowRegistry) {
registry.clear();
}
fn current_native_window_dispatch_context() -> Option<NativeWindowDispatchContext> {
CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| stack.borrow().last().cloned())
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
fn with_native_window_dispatch_context<R>(
context: NativeWindowDispatchContext,
f: impl FnOnce() -> R,
) -> R {
struct DispatchContextGuard;
impl Drop for DispatchContextGuard {
fn drop(&mut self) {
CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| {
stack.borrow_mut().pop();
});
}
}
CURRENT_NATIVE_WINDOW_DISPATCH.with(|stack| {
stack.borrow_mut().push(context);
});
let _guard = DispatchContextGuard;
f()
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn with_native_window_drag_handler<R>(
handler: NativeWindowDragHandler,
resize_handler: NativeWindowResizeHandler,
f: impl FnOnce() -> R,
) -> R {
let mut context = current_native_window_dispatch_context().unwrap_or_default();
context.drag_handler = Some(handler);
context.resize_handler = Some(resize_handler);
with_native_window_dispatch_context(context, f)
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn register_native_window(
key: NativeWindowKey,
options: NativeWindowOptions,
events: NativeWindowEvents,
state: Option<WindowState>,
root: NativeWindowRootHandle,
owner: NativeWindowOwner,
) {
let Some(registry) = current_native_window_registry() else {
log::error!(
"native window declaration {key:?} ignored because no native-window registry is active"
);
return;
};
registry.register(NativeWindowRegistration {
key,
options,
events,
state,
root,
owner,
});
}
#[cfg(all(
feature = "desktop-shell",
feature = "renderer-wgpu",
not(target_arch = "wasm32")
))]
pub(crate) fn unregister_native_window(key: NativeWindowKey, owner: NativeWindowOwner) {
let Some(registry) = current_native_window_registry() else {
log::error!(
"native window declaration {key:?} could not unregister because no native-window registry is active"
);
return;
};
registry.unregister(key, owner);
}
#[cfg(test)]
#[path = "tests/native_window_tests.rs"]
mod tests;