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/contexts.rs b/masonry/src/contexts.rs index 94494eb47..917e07846 100644 --- a/masonry/src/contexts.rs +++ b/masonry/src/contexts.rs @@ -6,9 +6,11 @@ use std::time::Duration; use accesskit::TreeUpdate; +use dpi::LogicalPosition; use parley::{FontContext, LayoutContext}; use tracing::{trace, warn}; use vello::kurbo::Vec2; +use winit::window::ResizeDirection; use crate::action::Action; use crate::passes::layout::run_layout_on; @@ -602,6 +604,58 @@ impl_context_method!( .emit_signal(RenderRootSignal::Action(action, self.widget_state.id)); } + /// Start a window drag. + /// + /// Moves the window with the left mouse button until the button is released. + pub fn drag_window(&mut self) { + trace!("drag_window"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::DragWindow); + } + + /// Start a window resize. + /// + /// Resizes the window with the left mouse button until the button is released. + pub fn drag_resize_window(&mut self, direction: ResizeDirection) { + trace!("drag_resize_window"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::DragResizeWindow(direction)); + } + + /// Toggle the maximized state of the window. + pub fn toggle_maximized(&mut self) { + trace!("toggle_maximized"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::ToggleMaximized); + } + + /// Minimize the window. + pub fn minimize(&mut self) { + trace!("minimize"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::Minimize); + } + + /// Exit the application. + pub fn exit(&mut self) { + trace!("exit"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::Exit); + } + + /// Show the window menu at a specified position. + pub fn show_window_menu(&mut self, position: LogicalPosition) { + trace!("show_window_menu"); + self.global_state + .signal_queue + .push_back(RenderRootSignal::ShowWindowMenu(position)); + } + /// Request a timer event. /// /// The return value is a token, which can be used to associate the diff --git a/masonry/src/event_loop_runner.rs b/masonry/src/event_loop_runner.rs index 207ddaa0e..68afef7a6 100644 --- a/masonry/src/event_loop_runner.rs +++ b/masonry/src/event_loop_runner.rs @@ -646,7 +646,7 @@ impl MasonryState<'_> { pub fn handle_memory_warning(&mut self, _: &ActiveEventLoop) {} // --- MARK: SIGNALS --- - fn handle_signals(&mut self, _event_loop: &ActiveEventLoop, app_driver: &mut dyn AppDriver) { + fn handle_signals(&mut self, event_loop: &ActiveEventLoop, app_driver: &mut dyn AppDriver) { let WindowState::Rendering { window, .. } = &mut self.window else { tracing::warn!("Tried to handle a signal whilst suspended or before window created"); return; @@ -691,6 +691,26 @@ impl MasonryState<'_> { render_root::RenderRootSignal::SetTitle(title) => { window.set_title(&title); } + render_root::RenderRootSignal::DragWindow => { + // TODO - Handle return value? + let _ = window.drag_window(); + } + render_root::RenderRootSignal::DragResizeWindow(direction) => { + // TODO - Handle return value? + let _ = window.drag_resize_window(direction); + } + render_root::RenderRootSignal::ToggleMaximized => { + window.set_maximized(!window.is_maximized()); + } + render_root::RenderRootSignal::Minimize => { + window.set_minimized(true); + } + render_root::RenderRootSignal::Exit => { + event_loop.exit(); + } + render_root::RenderRootSignal::ShowWindowMenu(position) => { + window.show_window_menu(position); + } } } } diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index 6ecffa6d9..d1fefd003 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -9,6 +9,7 @@ use parley::{FontContext, LayoutContext}; use tracing::warn; use vello::kurbo::{self, Rect}; use vello::Scene; +use winit::window::ResizeDirection; #[cfg(not(target_arch = "wasm32"))] use std::time::Instant; @@ -114,6 +115,12 @@ pub enum RenderRootSignal { SetCursor(CursorIcon), SetSize(PhysicalSize), SetTitle(String), + DragWindow, + DragResizeWindow(ResizeDirection), + ToggleMaximized, + Minimize, + Exit, + ShowWindowMenu(LogicalPosition), } impl RenderRoot { diff --git a/masonry/src/theme.rs b/masonry/src/theme.rs index 9241413b8..fec2dee7f 100644 --- a/masonry/src/theme.rs +++ b/masonry/src/theme.rs @@ -13,6 +13,8 @@ use crate::Insets; // They're picked for visual distinction and accessibility (99 percent) pub const WINDOW_BACKGROUND_COLOR: Color = Color::rgb8(0x29, 0x29, 0x29); +pub const TITLE_BAR_HEIGHT: f64 = 32.0; +pub const TITLE_BAR_COLOR: Color = Color::rgb8(0x1a, 0x1a, 0x1a); pub const TEXT_COLOR: Color = Color::rgb8(0xf0, 0xf0, 0xea); pub const DISABLED_TEXT_COLOR: Color = Color::rgb8(0xa0, 0xa0, 0x9a); pub const PLACEHOLDER_COLOR: Color = Color::rgb8(0x80, 0x80, 0x80); diff --git a/masonry/src/widget/center_box.rs b/masonry/src/widget/center_box.rs new file mode 100644 index 000000000..3f33f165c --- /dev/null +++ b/masonry/src/widget/center_box.rs @@ -0,0 +1,113 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A center box widget. + +use accesskit::{NodeBuilder, Role}; +use smallvec::SmallVec; +use tracing::{trace_span, Span}; +use vello::kurbo::Point; +use vello::Scene; + +use crate::widget::WidgetPod; +use crate::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +const PADDING: f64 = 10.; + +/// A center box widget. +pub struct CenterBox { + child: Option>>, + end: Option>>, +} + +// --- MARK: BUILDERS --- +impl CenterBox { + // TODO: Maybe the end widget should be optional here + pub(crate) fn new(child: impl Widget, end: impl Widget) -> Self { + Self { + child: Some(WidgetPod::new(child).boxed()), + end: Some(WidgetPod::new(end).boxed()), + } + } +} + +// --- MARK: IMPL WIDGET --- +impl Widget for CenterBox { + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + + 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, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} + + fn register_children(&mut self, ctx: &mut crate::RegisterCtx) { + if let Some(ref mut child) = self.child { + ctx.register_child(child); + } + + if let Some(ref mut end) = self.end { + ctx.register_child(end); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + let child = self.child.as_mut().unwrap(); + let end = self.end.as_mut().unwrap(); + + let child_size = ctx.run_layout(child, &bc.loosen()); + let end_size = ctx.run_layout(end, &bc.loosen()); + + let box_size = bc.constrain(Size::new( + child_size.width + end_size.width, + child_size.height.max(end_size.height), + )); + + ctx.place_child( + child, + Point::new( + (box_size.width / 2.0) - (child_size.width / 2.0), + (box_size.height / 2.0) - (child_size.height / 2.0), + ), + ); + + ctx.place_child( + end, + Point::new( + box_size.width - end_size.width - PADDING, + (box_size.height / 2.0) - (end_size.height / 2.0), + ), + ); + + box_size + } + + fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {} + + fn accessibility_role(&self) -> Role { + Role::GenericContainer + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut NodeBuilder) {} + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + let mut vec = SmallVec::new(); + + if let Some(child) = &self.child { + vec.push(child.id()); + } + + if let Some(end) = &self.end { + vec.push(end.id()); + } + + vec + } + + fn make_trace_span(&self) -> Span { + trace_span!("CenterBox") + } +} diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 433110f3e..0048279c4 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -15,6 +15,7 @@ mod tests; mod align; mod button; +mod center_box; mod checkbox; mod flex; mod grid; @@ -29,8 +30,12 @@ mod sized_box; mod spinner; mod split; mod textbox; +mod title_bar; mod variable_label; mod widget_arena; +mod window_button; +mod window_decorations; +mod window_handle; pub use self::image::Image; pub use align::Align; @@ -48,14 +53,20 @@ pub use sized_box::SizedBox; pub use spinner::Spinner; pub use split::Split; pub use textbox::Textbox; +pub use title_bar::TitleBar; pub use variable_label::VariableLabel; 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; +use center_box::CenterBox; +use window_button::{WindowButton, WindowButtonType}; + use crate::{Affine, Size}; // These are based on https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit diff --git a/masonry/src/widget/title_bar.rs b/masonry/src/widget/title_bar.rs new file mode 100644 index 000000000..c6f52b565 --- /dev/null +++ b/masonry/src/widget/title_bar.rs @@ -0,0 +1,89 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A titlebar widget. + +use accesskit::{NodeBuilder, Role}; +use smallvec::{smallvec, SmallVec}; +use tracing::{trace_span, Span}; +use vello::kurbo::Point; +use vello::Scene; + +use crate::paint_scene_helpers::fill_color; +use crate::widget::WidgetPod; +use crate::{ + theme, AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +use super::{CenterBox, Flex, Label, WindowButton, WindowButtonType, WindowHandle}; + +/// A titlebar widget. +pub struct TitleBar { + child: WidgetPod, +} + +// --- MARK: BUILDERS --- +impl TitleBar { + pub fn new() -> Self { + let title = CenterBox::new( + // TODO: Get the title from the window + Label::new("Title"), + Flex::row() + .with_child(WindowButton::new(WindowButtonType::Minimize)) + .with_child(WindowButton::new(WindowButtonType::Maximize)) + .with_child(WindowButton::new(WindowButtonType::Close)), + ); + + let handle = WindowHandle::new(title); + + Self { + child: WidgetPod::new(handle), + } + } +} + +// --- MARK: IMPL WIDGET --- +impl Widget for TitleBar { + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + + 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, _ctx: &mut LifeCycleCtx, _event: &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 child_size = ctx.run_layout(&mut self.child, bc); + let size = bc.constrain(( + child_size.width, + child_size.height.max(theme::TITLE_BAR_HEIGHT), + )); + + ctx.place_child(&mut self.child, Point::ORIGIN); + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + let bounds = ctx.size().to_rect(); + fill_color(scene, &bounds, theme::TITLE_BAR_COLOR); + } + + fn accessibility_role(&self) -> Role { + Role::TitleBar + } + + 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!("TitleBar") + } +} diff --git a/masonry/src/widget/window_button.rs b/masonry/src/widget/window_button.rs new file mode 100644 index 000000000..bef1c9b69 --- /dev/null +++ b/masonry/src/widget/window_button.rs @@ -0,0 +1,99 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A window button. + +use accesskit::{NodeBuilder, Role}; +use smallvec::{smallvec, SmallVec}; +use tracing::{trace_span, Span}; +use vello::kurbo::Point; +use vello::Scene; + +use crate::widget::WidgetPod; +use crate::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +use super::Label; + +pub enum WindowButtonType { + Close, + Maximize, + Minimize, +} + +/// A window button. +pub struct WindowButton { + child: WidgetPod