From b1dbfc14928d77e882146f73ead49972d1319675 Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Sun, 22 Sep 2024 23:31:22 +0200 Subject: [PATCH 1/4] Add signals for window management --- masonry/src/contexts.rs | 54 ++++++++++++++++++++++++++++++++ masonry/src/event_loop_runner.rs | 22 ++++++++++++- masonry/src/render_root.rs | 7 +++++ 3 files changed, 82 insertions(+), 1 deletion(-) 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 { From 09a927d70ea3308d605d90918e45d0e76150eb72 Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Sun, 22 Sep 2024 23:35:23 +0200 Subject: [PATCH 2/4] Add WindowHandle widget --- masonry/src/widget/mod.rs | 2 + masonry/src/widget/window_handle.rs | 132 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 masonry/src/widget/window_handle.rs diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 433110f3e..7965a1da5 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -31,6 +31,7 @@ mod split; mod textbox; mod variable_label; mod widget_arena; +mod window_handle; pub use self::image::Image; pub use align::Align; @@ -53,6 +54,7 @@ pub use widget_mut::WidgetMut; pub use widget_pod::WidgetPod; pub use widget_ref::WidgetRef; pub use widget_state::WidgetState; +pub use window_handle::WindowHandle; pub(crate) use widget_arena::WidgetArena; diff --git a/masonry/src/widget/window_handle.rs b/masonry/src/widget/window_handle.rs new file mode 100644 index 000000000..d82b3a421 --- /dev/null +++ b/masonry/src/widget/window_handle.rs @@ -0,0 +1,132 @@ +// Copyright 2024 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A titlebar area widget. + +use accesskit::{NodeBuilder, Role}; +use dpi::LogicalPosition; +use smallvec::{smallvec, SmallVec}; +use tracing::{trace_span, Span}; +use vello::kurbo::Point; +use vello::Scene; + +use crate::event::PointerButton; +use crate::widget::{WidgetMut, WidgetPod}; +use crate::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycleCtx, PaintCtx, + PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +const DRAG_OFFSET: f64 = 5.0; + +/// A titlebar area widget. +/// +/// An area that can be dragged to move the window. +pub struct WindowHandle { + child: Option>>, + last_pos: Option>, +} + +// --- MARK: BUILDERS --- +impl WindowHandle { + pub fn new(child: impl Widget) -> Self { + Self { + child: Some(WidgetPod::new(child).boxed()), + last_pos: None, + } + } +} + +// --- MARK: WIDGETMUT --- +impl WidgetMut<'_, WindowHandle> { + pub fn set_child(&mut self, child: impl Widget) { + if let Some(child) = self.widget.child.take() { + self.ctx.remove_child(child); + } + self.widget.child = Some(WidgetPod::new(child).boxed()); + self.ctx.children_changed(); + self.ctx.request_layout(); + } + + pub fn remove_child(&mut self) { + if let Some(child) = self.widget.child.take() { + self.ctx.remove_child(child); + } + } +} + +// --- MARK: IMPL WIDGET --- +impl Widget for WindowHandle { + fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { + match event { + PointerEvent::PointerDown(button, state) => { + if !ctx.is_disabled() { + match button { + PointerButton::Primary => self.last_pos = Some(state.position), + PointerButton::Secondary => ctx.show_window_menu(state.position), + _ => (), + } + } + } + PointerEvent::PointerMove(state) => { + if let Some(last_pos) = self.last_pos { + let distance = ((state.position.x - last_pos.x).powi(2) + + (state.position.y - last_pos.y).powi(2)) + .sqrt(); + + if distance >= DRAG_OFFSET { + ctx.drag_window(); + self.last_pos = None; + } + } + } + PointerEvent::PointerLeave(_) | PointerEvent::PointerUp(_, _) => { + self.last_pos = None; + } + _ => (), + } + } + + 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); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + match self.child.as_mut() { + Some(child) => { + let size = ctx.run_layout(child, bc); + ctx.place_child(child, Point::ORIGIN); + size + } + None => bc.max(), + } + } + + 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]> { + if let Some(child) = &self.child { + smallvec![child.id()] + } else { + SmallVec::new() + } + } + + fn make_trace_span(&self) -> Span { + trace_span!("WindowHandle") + } +} From 362a21600852bec5dfb8ed4bf48e2bb564e668b0 Mon Sep 17 00:00:00 2001 From: Marco Melorio Date: Mon, 23 Sep 2024 00:19:54 +0200 Subject: [PATCH 3/4] Add TitleBar widget. --- masonry/src/theme.rs | 2 + masonry/src/widget/center_box.rs | 113 ++++++++++++++++++++++++++++ masonry/src/widget/mod.rs | 7 ++ masonry/src/widget/title_bar.rs | 89 ++++++++++++++++++++++ masonry/src/widget/window_button.rs | 99 ++++++++++++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 masonry/src/widget/center_box.rs create mode 100644 masonry/src/widget/title_bar.rs create mode 100644 masonry/src/widget/window_button.rs 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 7965a1da5..19bdc8cf8 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,10 @@ mod sized_box; mod spinner; mod split; mod textbox; +mod title_bar; mod variable_label; mod widget_arena; +mod window_button; mod window_handle; pub use self::image::Image; @@ -49,6 +52,7 @@ 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; @@ -58,6 +62,9 @@ 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