diff options
| author | srdusr <[email protected]> | 2024-11-01 09:54:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-11-01 09:54:00 +0200 |
| commit | fe92f6e6b8ab680c9603cc6bcf16c98555c378ae (patch) | |
| tree | ea1d70cc060dd5aed0d66b7c17b075112378adf4 /crates/wayland/src | |
| parent | 4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c (diff) | |
| download | srdwm-fe92f6e6b8ab680c9603cc6bcf16c98555c378ae.tar.gz srdwm-fe92f6e6b8ab680c9603cc6bcf16c98555c378ae.zip | |
Add Snap-Layouts flyout (Windows 11-style maximize-button hover menu)
Right-clicking (or hovering) the maximize button opens a flyout of
fixed half/quarter positions (snap_flyout.rs renders it); picking one
applies that zone via WindowManager::apply_snap_zone, the click-driven
equivalent of dragging a window to that edge and releasing.
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/snap_flyout.rs | 100 | ||||
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 39 | ||||
| -rw-r--r-- | crates/wayland/src/state/menu.rs | 28 |
3 files changed, 164 insertions, 3 deletions
diff --git a/crates/wayland/src/snap_flyout.rs b/crates/wayland/src/snap_flyout.rs new file mode 100644 index 0000000..3064d0e --- /dev/null +++ b/crates/wayland/src/snap_flyout.rs @@ -0,0 +1,100 @@ +//! Snap-Layouts flyout - a Windows-11-style "pick where this window goes" +//! grid, opened by right-clicking a titlebar's maximize button. Scoped to +//! picking a position for the *one* window whose button was clicked, not +//! the full Windows 11 multi-window arrangement picker (choosing a preset +//! that places several windows into complementary zones at once) - see +//! `srdwm_core::SnapZoneKind`'s own doc comment for why that bigger feature +//! is deliberately out of scope here. +//! +//! Deliberately plain, matching `context_menu.rs`'s own stated bar: a fixed +//! 3-column by 2-row grid of six labeled cells, no live preview thumbnails, +//! no drag-to-multi-zone. See `decoration::render_snap_flyout` for the +//! actual pixels. + +use srdwm_core::{SnapZoneKind, WindowId}; + +pub(crate) struct SnapFlyout { + pub(crate) window: WindowId, + /// Top-left corner, in global (output-independent) space - same frame + /// `Window.geometry` and every other `custom_elements` position uses. + pub(crate) pos: (i32, i32), + pub(crate) cell_width: u32, + pub(crate) cell_height: u32, +} + +const COLUMNS: i32 = 3; +const ROWS: i32 = 2; +const CELL_WIDTH: u32 = 90; +const CELL_HEIGHT: u32 = 60; + +impl SnapFlyout { + /// Opens the flyout for `window`, top-left corner at `pos` - by + /// convention the maximize button's own titlebar position, matching + /// `ContextMenu::open`'s "wherever the click landed" convention. + pub(crate) fn open(window: WindowId, pos: (i32, i32)) -> Self { + Self { window, pos, cell_width: CELL_WIDTH, cell_height: CELL_HEIGHT } + } + + pub(crate) fn width(&self) -> u32 { + self.cell_width * COLUMNS as u32 + } + + pub(crate) fn height(&self) -> u32 { + self.cell_height * ROWS as u32 + } + + /// Grid cell order, left-to-right then top-to-bottom: halves on the + /// left column pair, quarters filling the remaining two columns -- + /// [`SnapZoneKind::ALL`]'s own declared order. + pub(crate) fn cells(&self) -> [SnapZoneKind; 6] { + SnapZoneKind::ALL + } + + /// Which zone (if any) global-space point `(x, y)` falls on. + pub(crate) fn zone_at(&self, x: i32, y: i32) -> Option<SnapZoneKind> { + let (rel_x, rel_y) = (x - self.pos.0, y - self.pos.1); + if rel_x < 0 || rel_x >= self.width() as i32 || rel_y < 0 || rel_y >= self.height() as i32 { + return None; + } + let col = rel_x / self.cell_width as i32; + let row = rel_y / self.cell_height as i32; + let index = (row * COLUMNS + col) as usize; + self.cells().get(index).copied() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zone_at_top_left_cell_is_the_first_zone_in_reading_order() { + let flyout = SnapFlyout::open(1, (100, 100)); + assert_eq!(flyout.zone_at(101, 101), Some(flyout.cells()[0])); + } + + #[test] + fn zone_at_maps_every_cell_to_a_distinct_zone() { + let flyout = SnapFlyout::open(1, (0, 0)); + let mut seen = Vec::new(); + for row in 0..ROWS { + for col in 0..COLUMNS { + let x = col * CELL_WIDTH as i32 + 1; + let y = row * CELL_HEIGHT as i32 + 1; + let zone = flyout.zone_at(x, y).unwrap_or_else(|| panic!("no zone at ({x},{y})")); + assert!(!seen.contains(&zone), "zone {zone:?} hit twice"); + seen.push(zone); + } + } + assert_eq!(seen.len(), 6); + } + + #[test] + fn zone_at_is_none_outside_the_flyouts_bounds() { + let flyout = SnapFlyout::open(1, (100, 100)); + assert_eq!(flyout.zone_at(99, 110), None, "just left of the flyout"); + assert_eq!(flyout.zone_at(100 + flyout.width() as i32, 110), None, "just right of the flyout"); + assert_eq!(flyout.zone_at(150, 99), None, "just above the flyout"); + assert_eq!(flyout.zone_at(150, 100 + flyout.height() as i32), None, "just below the flyout"); + } +} diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs index 0d301dc..db2de44 100644 --- a/crates/wayland/src/state/lifecycle.rs +++ b/crates/wayland/src/state/lifecycle.rs @@ -74,6 +74,33 @@ impl CompState { let Some(w) = self.wm.borrow().window(id).cloned() else { return }; let focused = self.wm.borrow().focused_id() == Some(id); let theme = self.wm.borrow().theme; + // `main.rs`'s `sync()` calls `Platform::redraw_decoration` - which + // always reaches here - for every visible window on every dirty + // tick, not only the window that actually changed (see `Comp + // State::decoration_signatures`'s own doc comment: a resize drag on + // one window re-renders every *other* open window's title text and + // border strips too, once per pointer-motion event, for pixels + // identical to what's already cached). Skipping the rebuild when + // nothing this function reads has actually changed since the last + // call turns those redundant calls into a cheap signature + // comparison instead of a full re-rasterization. + let signature = DecorationSignature { + width: w.geometry.width, + height: w.geometry.height, + decorated: w.decorated, + focused, + title: w.title.clone(), + border_color: w.border_color, + border_width: w.border_width, + corner_radius: w.corner_radius, + maximized: w.maximized, + fullscreen: w.fullscreen, + shadows_enabled: self.wm.borrow().shadows_enabled, + }; + if self.decoration_signatures.get(&id) == Some(&signature) { + return; + } + self.decoration_signatures.insert(id, signature); if w.decorated { let fg = if focused { theme.titlebar_fg_focused } else { theme.titlebar_fg_unfocused }; let width = w.geometry.width.max(1); @@ -82,7 +109,7 @@ impl CompState { // cut, so there's no more square-frame-around-a-round-titlebar // clash to avoid. See `render_titlebar`'s `round_corners` doc // comment. - let data = decoration::render_titlebar(width, TITLEBAR_HEIGHT, &w.title, theme.titlebar_bg, fg, true); + let data = decoration::render_titlebar(width, TITLEBAR_HEIGHT, &w.title, theme.titlebar_bg, fg, true, w.corner_radius); let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (width as i32, TITLEBAR_HEIGHT as i32), 1, Transform::Normal, None); self.decorations.insert(id, buffer); } else { @@ -102,7 +129,7 @@ impl CompState { let color = effective_border_color(w.border_color, focused); let strips = decoration::border_strips(w.geometry, w.border_width); if strips[0].width > 0 && strips[0].height > 0 { - let data = decoration::render_border_top(strips[0].width, w.border_width, color); + let data = decoration::render_border_top(strips[0].width, w.border_width, color, w.corner_radius); let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (strips[0].width as i32, w.border_width as i32), 1, Transform::Normal, None); self.border_top_decorations.insert(id, buffer); @@ -110,7 +137,7 @@ impl CompState { self.border_top_decorations.remove(&id); } if strips[1].width > 0 && strips[1].height > 0 { - let data = decoration::render_border_bottom(strips[1].width, w.border_width, color); + let data = decoration::render_border_bottom(strips[1].width, w.border_width, color, w.corner_radius); let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (strips[1].width as i32, w.border_width as i32), 1, Transform::Normal, None); self.border_bottom_decorations.insert(id, buffer); @@ -150,6 +177,7 @@ impl CompState { self.border_bottom_decorations.remove(&id); self.shadow_buffers.remove(&id); self.border_side_buffers.remove(&id); + self.decoration_signatures.remove(&id); self.last_synced_size.remove(&id); self.content_epoch.remove(&id); self.rounded_content_buffers.remove(&id); @@ -161,6 +189,11 @@ impl CompState { if self.context_menu.as_ref().is_some_and(|m| m.window == id) { self.close_context_menu(); } + // Same reasoning as the context menu above, for the Snap-Layouts + // flyout. + if self.snap_flyout.as_ref().is_some_and(|f| f.window == id) { + self.close_snap_flyout(); + } self.wm.borrow_mut().remove_window(id); self.pending.borrow_mut().push(CoreEvent::WindowDestroyed(id)); foreign_toplevel::window_closed(self, id); diff --git a/crates/wayland/src/state/menu.rs b/crates/wayland/src/state/menu.rs index ccd784e..9f645a8 100644 --- a/crates/wayland/src/state/menu.rs +++ b/crates/wayland/src/state/menu.rs @@ -56,6 +56,34 @@ impl CompState { } } + /// Opens the Snap-Layouts flyout for `window`, top-left corner at `pos` + /// (global space, by convention the maximize button's own titlebar + /// position). Same build-once-on-open pattern as `open_context_menu`. + pub(crate) fn open_snap_flyout(&mut self, window: WindowId, pos: (i32, i32)) { + let flyout = crate::snap_flyout::SnapFlyout::open(window, pos); + let theme = self.wm.borrow().theme; + let labels: Vec<&str> = flyout.cells().iter().map(|z| z.label()).collect(); + let data = decoration::render_snap_flyout(3, flyout.cell_width, flyout.cell_height, &labels, theme.titlebar_bg, theme.titlebar_fg_focused, theme.default_border_color); + let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (flyout.width() as i32, flyout.height() as i32), 1, Transform::Normal, None); + self.snap_flyout_buffer = Some(buffer); + self.snap_flyout = Some(flyout); + } + + pub(crate) fn close_snap_flyout(&mut self) { + self.snap_flyout = None; + self.snap_flyout_buffer = None; + } + + /// Applies `zone` to `window` - the flyout's own equivalent of + /// `run_context_menu_action`, taking the target explicitly rather than + /// reading `self.snap_flyout` for the same borrow-conflict reason + /// documented on that function. + pub(crate) fn run_snap_flyout_action(&mut self, window: WindowId, zone: SnapZoneKind) { + self.wm.borrow_mut().apply_snap_zone(window, zone); + self.sync_geometry(window); + foreign_toplevel::send_state(self, window); + } + /// True when this titlebar press is the second of a double-click on the /// same window. Threshold is the usual 400ms. pub(crate) fn is_double_click(&mut self, id: WindowId, time: u32) -> bool { |