Toplevel events name the window, apps hear the pointer hover a surface and where their blurred cards were laid out, and rows can align to the bottom
A shell could not tell the windows of an app apart, follow the mouse over one of its overlays, or tell its compositor where the translucent cards of a surface ended up. crustace's floating dock needs all three, to activate and close single windows, to magnify the icons under the pointer and to have forge blur behind its pill and its Quick Settings cards.
`ToplevelEvent::Opened` carries the handle's `identifier`, the compositor's token for that one window, and its `title`, both from `ForeignToplevelInfo`; `toplevel_opened` builds the event for the new and the updated handle. Matches on `Opened { id, app_id }` have to add the fields or `..`.
`App::on_pointer_hover( SurfaceTarget, Option<( x, y )> )` is called on every mouse motion over a surface, in its physical pixels, and with `None` from the pointer's `leave`, which ltk did not handle before. `SurfaceFocus::target` gives the `SurfaceTarget` of a focus.
`Container::blur_behind()` marks a container whose rect and largest corner radius, capped at half its shorter side, the layout pass records in surface coordinates through the `DrawCtx::origin` offset of the sub-canvases, clipped by the viewports it sits in. `commit_draw_ctx` keeps them in `FrameState`, and after a frame where they changed `draw_frame` hands them to `App::on_blur_regions( SurfaceTarget, &[ ( Rect, f32 ) ] )`, for the main surface and each overlay.
`Row::align_bottom()` pins the children to the bottom edge, so taller siblings grow upward. `docs/architecture.md` lists `on_pointer_hover` among the input hooks.
Not done. `on_blur_regions`, `blur_behind` and `align_bottom` are not in the documentation, and there are no tests for them. The regions are reported after the frame is committed, so a compositor that receives them over IPC is one frame behind a surface whose cards move.
This commit is contained in:
@@ -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**
|
||||
|
||||
|
||||
+12
-2
@@ -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
|
||||
|
||||
@@ -414,6 +414,7 @@ impl<Msg: Clone> UiSurface<Msg>
|
||||
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 );
|
||||
|
||||
+16
-1
@@ -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<Msg: Clone>(
|
||||
{
|
||||
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<Msg: Clone>(
|
||||
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::<Msg>( 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<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();
|
||||
for mut w in new_rects
|
||||
{
|
||||
|
||||
@@ -100,6 +100,8 @@ pub( crate ) struct DrawCtx<Msg: Clone>
|
||||
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<Msg: Clone>(
|
||||
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<Msg: Clone>(
|
||||
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
|
||||
|
||||
@@ -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<A: App>( data: &mut AppData<A> ) -> 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<A: App>( data: &mut AppData<A> ) -> 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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-10
@@ -710,10 +710,10 @@ impl<A: App> ProvidesRegistryState for AppData<A>
|
||||
/// `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<A: App> ForeignToplevelListHandler for AppData<A>
|
||||
@@ -741,9 +745,8 @@ impl<A: App> ForeignToplevelListHandler for AppData<A>
|
||||
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<A: App> ForeignToplevelListHandler for AppData<A>
|
||||
// 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 );
|
||||
}
|
||||
|
||||
@@ -330,6 +330,7 @@ fn empty_draw_ctx<Msg: Clone>() -> DrawCtx<Msg>
|
||||
accessible_extras: Vec::new(),
|
||||
live_depth: 0,
|
||||
origin: ( 0.0, 0.0 ),
|
||||
blur_regions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Msg: Clone>
|
||||
pub prev_focused: Option<usize>,
|
||||
pub prev_hovered: Option<usize>,
|
||||
pub prev_pressed: Option<usize>,
|
||||
/// `blur_behind` containers of the last frame, and whether they changed since reported.
|
||||
pub blur_regions: Vec<( Rect, f32 )>,
|
||||
pub blur_changed: bool,
|
||||
}
|
||||
|
||||
impl<Msg: Clone> FrameState<Msg>
|
||||
@@ -209,6 +224,8 @@ impl<Msg: Clone> FrameState<Msg>
|
||||
prev_focused: None,
|
||||
prev_hovered: None,
|
||||
prev_pressed: None,
|
||||
blur_regions: Vec::new(),
|
||||
blur_changed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ impl<A: App> PointerHandler for AppData<A>
|
||||
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 ),
|
||||
|
||||
@@ -29,6 +29,7 @@ impl<A: App> AppData<A>
|
||||
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).
|
||||
|
||||
@@ -48,6 +48,7 @@ pub struct Row<Msg: Clone>
|
||||
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<Msg: Clone> Row<Msg>
|
||||
padding: Length::px( 0.0 ),
|
||||
align_right: false,
|
||||
align_top: false,
|
||||
align_bottom: false,
|
||||
fill_height: false,
|
||||
shrink: true,
|
||||
}
|
||||
@@ -134,6 +136,13 @@ impl<Msg: Clone> Row<Msg>
|
||||
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<Msg: Clone> Row<Msg>
|
||||
( 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<Msg: Clone> Row<Msg>
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ pub struct Container<Msg: Clone>
|
||||
/// 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<Msg: Clone> Container<Msg>
|
||||
@@ -118,9 +120,19 @@ impl<Msg: Clone> Container<Msg>
|
||||
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<Msg: Clone> Container<Msg>
|
||||
border: self.border,
|
||||
max_width: self.max_width,
|
||||
a11y_live: self.a11y_live,
|
||||
blur_behind: self.blur_behind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user