diff --git a/docs/architecture.md b/docs/architecture.md index f4a2598..8795903 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -102,6 +102,7 @@ In practice, that model is easiest to adopt in three steps: - `on_mouse_button( &MouseButtonEvent )` — every press and release of the left, middle and right buttons (`MouseButton`), with the position in physical pixels and Ctrl, Shift and Alt, before ltk handles it: for an app that hands the mouse on, as a terminal does to a program that asks for it. - `on_modifiers( ctrl, shift, alt )` — the modifiers held changed, for an app that needs them with the wheel or a pointer motion, which do not carry them. - `on_keyboard_leave()` — the window lost the keyboard, so no release will come for the keys still held: an app that repeats keys on its own timer stops there, and forgets the modifiers it kept. +- `on_pointer_hover( SurfaceTarget, Option<( x, y )> )` — the mouse moved over that surface, in its physical pixels, or left it with `None`: for hover effects that follow the pointer, as a dock magnifying the icons under it. **Implement when the app has more than one place to be** diff --git a/src/app.rs b/src/app.rs index 01f382d..8701a84 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,11 +14,12 @@ use crate::widget::Element; /// compositor sends `closed` and the runtime is about to destroy the /// handle proxy. Both carry the same `id`, the Wayland protocol id of /// the handle, unique within the session and stable for the handle's -/// lifetime. +/// lifetime. `identifier` is the compositor's opaque per-window token, +/// the one to name a single window by when talking to the compositor. #[ derive( Debug, Clone ) ] pub enum ToplevelEvent { - Opened { id: u32, app_id: String }, + Opened { id: u32, app_id: String, identifier: String, title: String }, Closed { id: u32 }, } @@ -949,6 +950,15 @@ pub trait App: 'static /// WPE WebView. fn on_pointer_move( &mut self, _x: f32, _y: f32 ) {} + /// Mouse motion over `target`, in physical pixels of that surface, or + /// `None` when the pointer leaves it. Touch never reports here. + fn on_pointer_hover( &mut self, _target: SurfaceTarget, _position: Option<( f32, f32 )> ) {} + + /// The `( rect, corner radius )` of every [`Container::blur_behind`](crate::widget::container::Container::blur_behind) + /// drawn on `target`, in its physical pixels, after a frame where they + /// changed: for a shell to tell its compositor what to blur. + fn on_blur_regions( &mut self, _target: SurfaceTarget, _regions: &[ ( crate::types::Rect, f32 ) ] ) {} + /// Called on a wheel / touchpad scroll event whose position does /// not fall inside any LTK [`scroll`](crate::scroll) viewport. `x`, /// `y` are in physical pixels (same coordinate space as diff --git a/src/core.rs b/src/core.rs index ef0ea59..7fef425 100644 --- a/src/core.rs +++ b/src/core.rs @@ -414,6 +414,7 @@ impl UiSurface accessible_extras: Vec::new(), live_depth: 0, origin: ( 0.0, 0.0 ), + blur_regions: Vec::new(), }; draw::layout_and_draw( element, &mut self.canvas, options.bounds, &mut ctx, 0 ); diff --git a/src/draw/layout.rs b/src/draw/layout.rs index 147df45..0fc3e53 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -11,7 +11,7 @@ //! function. use crate::render::Canvas; -use crate::types::Rect; +use crate::types::{ Corners, Rect }; use crate::widget::{ Element, LaidOutWidget, WidgetHandlers }; use super::DrawCtx; @@ -245,6 +245,13 @@ pub( crate ) fn layout_and_draw( { canvas.stroke_rect( rect, color, width, c.corners ); } + if c.blur_behind + { + let Corners { tl, tr, br, bl } = c.corners; + let radius = tl.max( tr ).max( br ).max( bl ).min( rect.width * 0.5 ).min( rect.height * 0.5 ); + let on_surface = Rect { x: rect.x + ctx.origin.0, y: rect.y + ctx.origin.1, ..rect }; + ctx.blur_regions.push( ( on_surface, radius ) ); + } let pad_l = canvas.resolve_geom( c.pad_left ); let pad_r = canvas.resolve_geom( c.pad_right ); let pad_t = canvas.resolve_geom( c.pad_top ); @@ -463,11 +470,19 @@ pub( crate ) fn layout_and_draw( let child_rect = Rect { x: 0.0, y: 0.0, width: rect.width, height: effective_h }; let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); + let blur_before = ctx.blur_regions.len(); let origin = ctx.origin; ctx.origin = ( origin.0 + rect.x, origin.1 + rect.y ); let next_idx = layout_and_draw::( v.child.as_ref(), &mut sub, child_rect, ctx, flat_idx ); ctx.origin = origin; + let view_on_surface = Rect { x: rect.x + origin.0, y: rect.y + origin.1, ..rect }; + let clipped: Vec<( Rect, f32 )> = ctx.blur_regions.drain( blur_before.. ) + .map( |( r, radius )| ( clamp_rect_to( r, view_on_surface ), radius ) ) + .filter( |( r, _ )| r.width > 0.0 && r.height > 0.0 ) + .collect(); + ctx.blur_regions.extend( clipped ); + let new_rects: Vec> = ctx.widget_rects.drain( rects_before.. ).collect(); for mut w in new_rects { diff --git a/src/draw/mod.rs b/src/draw/mod.rs index 2cebdf9..83c2bc6 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -100,6 +100,8 @@ pub( crate ) struct DrawCtx pub live_depth: u32, /// Where the canvas being drawn on sits on the surface: a scroll or a viewport draws its child on a canvas of its own. pub origin: ( f32, f32 ), + /// `( rect, radius )` of every `blur_behind` container, in surface coordinates. + pub blur_regions: Vec<( Rect, f32 )>, } /// Build the per-frame [`DrawCtx`] from the surface's [`FrameState`], moving the @@ -136,6 +138,7 @@ pub( crate ) fn build_draw_ctx( accessible_extras: Vec::new(), live_depth: 0, origin: ( 0.0, 0.0 ), + blur_regions: Vec::new(), } } @@ -167,6 +170,11 @@ pub( crate ) fn commit_draw_ctx( frame.accessible_extras = ctx.accessible_extras; frame.scroll_navigable_items = ctx.scroll_navigable_items; frame.scroll_clipped = ctx.scroll_clipped; + if ctx.blur_regions != frame.blur_regions + { + frame.blur_regions = ctx.blur_regions; + frame.blur_changed = true; + } } /// Paint the built-in Copy / Cut / Paste context menu on top of the diff --git a/src/event_loop/frame.rs b/src/event_loop/frame.rs index 18976b5..7964df7 100644 --- a/src/event_loop/frame.rs +++ b/src/event_loop/frame.rs @@ -6,7 +6,7 @@ use smithay_client_toolkit::compositor::CompositorState; use smithay_client_toolkit::reexports::client::protocol::{ wl_shm, wl_surface::WlSurface }; use smithay_client_toolkit::shm::Shm; -use crate::app::App; +use crate::app::{ App, SurfaceTarget }; use crate::egl_context::EglContext; use crate::types::{ Color, Rect }; use crate::widget::Element; @@ -77,6 +77,10 @@ pub( crate ) fn draw_frame( data: &mut AppData ) -> bool first_commit_now = true; } data.app.on_main_frame_committed(); + if std::mem::take( &mut data.main.frame.blur_changed ) + { + data.app.on_blur_regions( SurfaceTarget::Main, &data.main.frame.blur_regions ); + } } for ( _, ss ) in data.secondaries.iter_mut() @@ -136,6 +140,10 @@ pub( crate ) fn draw_frame( data: &mut AppData ) -> bool ss.needs_redraw = false; ss.last_draw = std::time::Instant::now(); data.app.on_overlay_frame_committed( spec.id ); + if std::mem::take( &mut ss.frame.blur_changed ) + { + data.app.on_blur_regions( SurfaceTarget::Overlay( spec.id ), &ss.frame.blur_regions ); + } } } diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 8ce5374..937bca4 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -710,10 +710,10 @@ impl ProvidesRegistryState for AppData /// `identifier` (always present, unique per handle). Without the cascade, /// any client that never set `app_id` (e.g. winit-windowed compositors /// before they bind their xdg surface) was silently invisible to the dock. -fn toplevel_display_id( +fn toplevel_opened( list: &ForeignToplevelList, handle: &ExtForeignToplevelHandleV1, -) -> String +) -> ToplevelEvent { // Only ever surface the client's `app_id`. The protocol-level // `identifier` is a server-internal random opaque token, not an @@ -723,8 +723,12 @@ fn toplevel_display_id( // the random identifier as their "app id" until the client's // real `set_app_id` lands. `title` is also unsuitable — it // changes every time the window's title text updates. - let Some( info ) = list.info( handle ) else { return String::new(); }; - info.app_id + let id = handle.id().protocol_id(); + match list.info( handle ) + { + Some( info ) => ToplevelEvent::Opened { id, app_id: info.app_id, identifier: info.identifier, title: info.title }, + None => ToplevelEvent::Opened { id, app_id: String::new(), identifier: String::new(), title: String::new() }, + } } impl ForeignToplevelListHandler for AppData @@ -741,9 +745,8 @@ impl ForeignToplevelListHandler for AppData handle: ExtForeignToplevelHandleV1, ) { - let id = handle.id().protocol_id(); - let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); - if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) + let event = toplevel_opened( &self.foreign_toplevel_list, &handle ); + if let Some( msg ) = self.app.on_toplevel_event( event ) { self.pending_msgs.push( msg ); } @@ -763,9 +766,8 @@ impl ForeignToplevelListHandler for AppData // can treat repeated opens as idempotent and pay nothing here, // but the app_id-can-change-mid-life case still produces a // correct sequence (old refcount drops, new one bumps). - let id = handle.id().protocol_id(); - let app_id = toplevel_display_id( &self.foreign_toplevel_list, &handle ); - if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } ) + let event = toplevel_opened( &self.foreign_toplevel_list, &handle ); + if let Some( msg ) = self.app.on_toplevel_event( event ) { self.pending_msgs.push( msg ); } diff --git a/src/event_loop/subsurface.rs b/src/event_loop/subsurface.rs index 19a981a..d5c2848 100644 --- a/src/event_loop/subsurface.rs +++ b/src/event_loop/subsurface.rs @@ -330,6 +330,7 @@ fn empty_draw_ctx() -> DrawCtx accessible_extras: Vec::new(), live_depth: 0, origin: ( 0.0, 0.0 ), + blur_regions: Vec::new(), } } diff --git a/src/event_loop/surface.rs b/src/event_loop/surface.rs index cb68b15..2167a33 100644 --- a/src/event_loop/surface.rs +++ b/src/event_loop/surface.rs @@ -22,7 +22,7 @@ use std::collections::HashMap; use std::sync::Arc; use super::app_data::AppData; -use crate::app::OverlayId; +use crate::app::{ OverlayId, SurfaceTarget }; use crate::egl_context::{ EglContext, EglSurface }; use crate::input::GestureState; use crate::render::Canvas; @@ -42,6 +42,18 @@ pub( crate ) enum SurfaceFocus Overlay( OverlayId ), } +impl SurfaceFocus +{ + pub( crate ) fn target( self ) -> SurfaceTarget + { + match self + { + SurfaceFocus::Main => SurfaceTarget::Main, + SurfaceFocus::Overlay( id ) => SurfaceTarget::Overlay( id ), + } + } +} + /// Configuration for a layer-shell surface, used both for the main surface /// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay /// returned by [`crate::app::App::overlays`]. @@ -187,6 +199,9 @@ pub( crate ) struct FrameState pub prev_focused: Option, pub prev_hovered: Option, pub prev_pressed: Option, + /// `blur_behind` containers of the last frame, and whether they changed since reported. + pub blur_regions: Vec<( Rect, f32 )>, + pub blur_changed: bool, } impl FrameState @@ -209,6 +224,8 @@ impl FrameState prev_focused: None, prev_hovered: None, prev_pressed: None, + blur_regions: Vec::new(), + blur_changed: false, } } } diff --git a/src/input/pointer/mod.rs b/src/input/pointer/mod.rs index 189c939..57e23c6 100644 --- a/src/input/pointer/mod.rs +++ b/src/input/pointer/mod.rs @@ -59,6 +59,7 @@ impl PointerHandler for AppData match event.kind { PointerEventKind::Enter { .. } => self.on_pointer_enter( conn, qh, pointer, event ), + PointerEventKind::Leave { .. } => self.app.on_pointer_hover( focus.target(), None ), PointerEventKind::Motion { .. } => self.on_pointer_motion( conn, qh, pointer, event ), PointerEventKind::Press { button: 0x110, .. } => self.on_pointer_left_press( conn, qh, pointer, event ), PointerEventKind::Press { button: 0x111, .. } => self.on_pointer_right_click( conn, qh, pointer, event ), diff --git a/src/input/pointer/motion.rs b/src/input/pointer/motion.rs index 019ddf7..333688c 100644 --- a/src/input/pointer/motion.rs +++ b/src/input/pointer/motion.rs @@ -29,6 +29,7 @@ impl AppData let pp = self.surface( focus ).to_physical( event.position.0, event.position.1 ); self.pointer_pos = pp; self.app.on_pointer_move( pp.x, pp.y ); + self.app.on_pointer_hover( focus.target(), Some( ( pp.x, pp.y ) ) ); if self.osk_move( focus, pp ) { return; } // Hover tracking — pointer-only (touch has no hover). diff --git a/src/layout/row.rs b/src/layout/row.rs index f7df914..1ac4d65 100644 --- a/src/layout/row.rs +++ b/src/layout/row.rs @@ -48,6 +48,7 @@ pub struct Row pub( crate ) padding: Length, pub( crate ) align_right: bool, pub( crate ) align_top: bool, + pub( crate ) align_bottom: bool, pub( crate ) fill_height: bool, /// When false the row keeps every child at its preferred width even if /// the cluster overflows. Default true. @@ -65,6 +66,7 @@ impl Row padding: Length::px( 0.0 ), align_right: false, align_top: false, + align_bottom: false, fill_height: false, shrink: true, } @@ -134,6 +136,13 @@ impl Row self } + /// Pin children to the bottom edge, so taller siblings grow upward. + pub fn align_bottom( mut self ) -> Self + { + self.align_bottom = true; + self + } + /// Stretch every child to the row's inner height, so siblings share /// both the top and the bottom line regardless of their natural /// heights. The row's own height still comes from the tallest @@ -306,6 +315,8 @@ impl Row ( rect.y + pad, inner_h ) } else if self.align_top { ( rect.y + pad, h ) + } else if self.align_bottom { + ( rect.y + pad + inner_h - h, h ) } else { ( rect.y + pad + ( inner_h - h ) / 2.0, h ) }; @@ -327,6 +338,7 @@ impl Row padding: self.padding, align_right: self.align_right, align_top: self.align_top, + align_bottom: self.align_bottom, fill_height: self.fill_height, shrink: self.shrink, } diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs index 5e87d14..32f0d62 100644 --- a/src/widget/container/mod.rs +++ b/src/widget/container/mod.rs @@ -98,6 +98,8 @@ pub struct Container /// toasts, status banners and OSDs that need to be read on /// appearance even when the user has not navigated to them. pub( crate ) a11y_live: bool, + /// Reported to [`crate::App::on_blur_regions`] so the compositor can blur what lies behind it. + pub( crate ) blur_behind: bool, } impl Container @@ -118,9 +120,19 @@ impl Container border: None, max_width: None, a11y_live: false, + blur_behind: false, } } + /// Report this container's rect and corner radius through + /// [`crate::App::on_blur_regions`], for a shell whose compositor blurs + /// what lies behind its translucent cards. + pub fn blur_behind( mut self ) -> Self + { + self.blur_behind = true; + self + } + pub fn live_region( mut self, live: bool ) -> Self { self.a11y_live = live; @@ -306,6 +318,7 @@ impl Container border: self.border, max_width: self.max_width, a11y_live: self.a11y_live, + blur_behind: self.blur_behind, } } }