diff --git a/masonry/examples/custom_decorations.rs b/masonry/examples/custom_decorations.rs new file mode 100644 index 000000000..c7e95c2db --- /dev/null +++ b/masonry/examples/custom_decorations.rs @@ -0,0 +1,54 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +// On Windows platform, don't show a console when opening the app. +#![windows_subsystem = "windows"] + +use masonry::app_driver::{AppDriver, DriverCtx}; +use masonry::dpi::LogicalSize; +use masonry::widget::{Button, CrossAxisAlignment, Flex, RootWidget, TitleBar, WindowDecorations}; +use masonry::{Action, WidgetId}; +use winit::window::Window; + +struct Driver; + +impl AppDriver for Driver { + fn on_action(&mut self, _ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) { + match action { + Action::ButtonPressed(_) => { + println!("Hello"); + } + action => { + eprintln!("Unexpected action {action:?}"); + } + } + } +} + +pub fn main() { + let title_bar = TitleBar::new(); + + let button = Button::new("Say hello"); + + let content = Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Fill) + .with_child(title_bar) + .with_flex_child(button, CrossAxisAlignment::Center); + + let main_widget = WindowDecorations::new(content); + + let window_size = LogicalSize::new(400.0, 400.0); + let window_attributes = Window::default_attributes() + .with_title("Hello World!") + .with_resizable(true) + .with_decorations(false) + .with_min_inner_size(window_size); + + masonry::event_loop_runner::run( + masonry::event_loop_runner::EventLoop::with_user_event(), + window_attributes, + RootWidget::new(main_widget), + Driver, + ) + .unwrap(); +} diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 19bdc8cf8..0048279c4 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -34,6 +34,7 @@ mod title_bar; mod variable_label; mod widget_arena; mod window_button; +mod window_decorations; mod window_handle; pub use self::image::Image; @@ -58,6 +59,7 @@ pub use widget_mut::WidgetMut; pub use widget_pod::WidgetPod; pub use widget_ref::WidgetRef; pub use widget_state::WidgetState; +pub use window_decorations::WindowDecorations; pub use window_handle::WindowHandle; pub(crate) use widget_arena::WidgetArena; diff --git a/masonry/src/widget/window_decorations.rs b/masonry/src/widget/window_decorations.rs new file mode 100644 index 000000000..17c5a0a1c --- /dev/null +++ b/masonry/src/widget/window_decorations.rs @@ -0,0 +1,113 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A container to show window decorations. + +use accesskit::{NodeBuilder, Role}; +use cursor_icon::CursorIcon; +use smallvec::{smallvec, SmallVec}; +use tracing::{trace_span, Span}; +use vello::kurbo::Insets; +use vello::Scene; +use winit::window::ResizeDirection; + +use crate::paint_scene_helpers::stroke; +use crate::widget::WidgetPod; +use crate::{ + theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +const BORDER_WIDTH: f64 = 2.0; +const INSETS: Insets = Insets::uniform(BORDER_WIDTH); + +/// A container to show window decorations. +pub struct WindowDecorations { + pub(crate) child: WidgetPod, +} + +impl WindowDecorations { + pub fn new(widget: W) -> WindowDecorations { + WindowDecorations { + child: WidgetPod::new(widget), + } + } +} + +impl Widget for WindowDecorations { + fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { + match event { + PointerEvent::PointerMove(state) => { + if state.position.y <= BORDER_WIDTH { + ctx.set_cursor(&CursorIcon::NResize); + } else if state.position.y >= ctx.size().height - BORDER_WIDTH { + ctx.set_cursor(&CursorIcon::SResize); + } else if state.position.x <= BORDER_WIDTH { + ctx.set_cursor(&CursorIcon::WResize); + } else if state.position.x >= ctx.size().width - BORDER_WIDTH { + ctx.set_cursor(&CursorIcon::EResize); + } + } + PointerEvent::PointerLeave(_) => { + ctx.set_cursor(&CursorIcon::Default); + } + PointerEvent::PointerDown(_, state) => { + if state.position.y <= BORDER_WIDTH { + ctx.drag_resize_window(ResizeDirection::North); + } else if state.position.y >= ctx.size().height - BORDER_WIDTH { + ctx.drag_resize_window(ResizeDirection::South); + } else if state.position.x <= BORDER_WIDTH { + ctx.drag_resize_window(ResizeDirection::West); + } else if state.position.x >= ctx.size().width - BORDER_WIDTH { + ctx.drag_resize_window(ResizeDirection::East); + } + } + _ => {} + } + } + + fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + + fn on_status_change(&mut self, _: &mut LifeCycleCtx, _: &StatusChange) {} + + fn register_children(&mut self, ctx: &mut crate::RegisterCtx) { + ctx.register_child(&mut self.child); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + let padding = Size::new(INSETS.x_value(), INSETS.y_value()); + let child_bc = bc.shrink(padding); + + let child_size = ctx.run_layout(&mut self.child, &child_bc); + + let size = bc.constrain(Size::new( + child_size.width + padding.width, + child_size.height + padding.height, + )); + + let child_offset = (size.to_vec2() - child_size.to_vec2()) / 2.0; + ctx.place_child(&mut self.child, child_offset.to_point()); + + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + let rect = ctx.size().to_rect().inset(-BORDER_WIDTH / 2.0); + stroke(scene, &rect, theme::BORDER_DARK, BORDER_WIDTH); + } + + fn accessibility_role(&self) -> Role { + Role::GenericContainer + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut NodeBuilder) {} + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + smallvec![self.child.id()] + } + + fn make_trace_span(&self) -> Span { + trace_span!("WindowDecorations") + } +}