Files
ltk/src/event_loop/cursor_shape.rs
Pedro M. de Echanove Pasquin d4d7ee742e
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect `glScissor` clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its `fill` / `clear_rects_transparent` scissor semantics. Adds software-backend pixel tests covering the snapping.
Font resolution unification. The system-font candidate chain, `find_font_opt` and `load_default_font_bytes` lived in two copies (`render/helpers` and `gles_render/helpers`) that had already diverged — one resolved through `find_font_opt`, the other inlined the candidate loop — and now live once in `system_fonts`. The two per-backend `OnceLock` default-font caches and `primary_handle` collapse into a single `system_fonts::default_handle`. Module docs and the `font_registry` caller are updated accordingly.
Shared image validation and rect inflation. `draw_image_data`'s dimension check and its one-line warning were byte-duplicated across both backends and are now `render::helpers::validate_rgba_dims`. The six manual symmetric `Rect`-inflate literals in the GLES primitives reuse the existing `Rect::expand`.
FrameState and DrawCtx de-duplication. The eleven `SurfaceState` fields the draw pass owns and threads through `DrawCtx` — `widget_rects`, the cursor / selection maps, the scroll state, `accessible_extras`, `prev_focused` / `prev_hovered` / `prev_pressed` — move into a `FrameState` sub-struct. The per-frame `build_draw_ctx` / `commit_draw_ctx` helpers can then borrow `&mut ss.frame`, disjoint from `ss.canvas` and `ss.pool`, so the four frame paths (software / GLES × full / partial) replace their duplicated `DrawCtx` construction and write-back with a single helper call each. A whole-`SurfaceState` borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. `content_dirty` stays on `SurfaceState` — it is an invalidation flag, not frame state — and is reset at the call site.
rich_text tests. Adds the previously-missing `tests.rs` for the `RichText` widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and `map_msg` range preservation — all headless against a software `Canvas`, with line counts forced by `\n` so they do not depend on any system font's measured width.
Documentation. Fills the rustdoc gaps on the embedder-facing surface: `core::UiSurface` accessors, the `egl_context` public API, the `GlesCanvas` methods, `theme::typography` and `theme::error`, and the `RichText` / `Text` builders; adds a crate-level "Rendering backends" overview. `CHANGELOG.md` is added (0.2.0 / 0.1.0), and `docs/widgets.md` / `docs/cookbook.md` gain `rich_text`, `external`, and the CPU-draw / path-clip / externally-laid-out-tree recipes. `debian/changelog` gets the 0.2.0-1 entry. Private intra-doc links to `system_fonts` are demoted to code spans so `cargo doc` is warning-free.
Packaging. The `libltk-dev` registry crate shipped a `Cargo.toml` declaring the `lookup` bench while the `Makefile` install copied only `src/`, so Cargo refused to parse the manifest over a missing `benches/lookup.rs`; the install now ships `benches/` as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). `Cargo.toml` is bumped to 0.2.0 to match the package version and the `ltk-0.2.0` registry directory.
2026-06-25 12:43:40 +02:00

222 lines
7.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;
use super::app_data::AppData;
use super::surface::{ SurfaceFocus, SurfaceKind };
use crate::app::App;
use crate::types::{ CursorShape, Point };
/// Classify a pointer position against a surface of size `w × h` (in
/// physical pixels). Returns the resize edge if the pointer is inside
/// the `border`-thick grab band along any edge or corner.
pub( crate ) fn resize_edge_for_pos( pos: Point, w: f32, h: f32, border: f32 ) -> Option<ResizeEdge>
{
let on_left = pos.x < border;
let on_right = pos.x > w - border;
let on_top = pos.y < border;
let on_bottom = pos.y > h - border;
Some( match ( on_top, on_bottom, on_left, on_right )
{
( true, _, true, _ ) => ResizeEdge::TopLeft,
( true, _, _, true ) => ResizeEdge::TopRight,
( _, true, true, _ ) => ResizeEdge::BottomLeft,
( _, true, _, true ) => ResizeEdge::BottomRight,
( true, _, _, _ ) => ResizeEdge::Top,
( _, true, _, _ ) => ResizeEdge::Bottom,
( _, _, true, _ ) => ResizeEdge::Left,
( _, _, _, true ) => ResizeEdge::Right,
_ => return None,
} )
}
/// Map an [`ResizeEdge`] to the [`CursorShape`](crate::types::CursorShape)
/// to display while hovering / dragging it.
pub( crate ) fn resize_edge_to_cursor( edge: ResizeEdge ) -> CursorShape
{
use CursorShape::*;
match edge
{
ResizeEdge::Top => NResize,
ResizeEdge::Bottom => SResize,
ResizeEdge::Left => WResize,
ResizeEdge::Right => EResize,
ResizeEdge::TopLeft => NwResize,
ResizeEdge::TopRight => NeResize,
ResizeEdge::BottomLeft => SwResize,
ResizeEdge::BottomRight => SeResize,
_ => Default,
}
}
impl<A: App> AppData<A>
{
/// Convert a public [`CursorShape`](crate::types::CursorShape) into
/// the wire-protocol [`Shape`] enum the compositor expects.
fn cursor_shape_to_wp( shape: CursorShape )
-> smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape
{
use CursorShape as C;
use smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape as S;
match shape
{
C::Default => S::Default,
C::ContextMenu => S::ContextMenu,
C::Help => S::Help,
C::Pointer => S::Pointer,
C::Progress => S::Progress,
C::Wait => S::Wait,
C::Cell => S::Cell,
C::Crosshair => S::Crosshair,
C::Text => S::Text,
C::VerticalText => S::VerticalText,
C::Alias => S::Alias,
C::Copy => S::Copy,
C::Move => S::Move,
C::NoDrop => S::NoDrop,
C::NotAllowed => S::NotAllowed,
C::Grab => S::Grab,
C::Grabbing => S::Grabbing,
C::EResize => S::EResize,
C::NResize => S::NResize,
C::NeResize => S::NeResize,
C::NwResize => S::NwResize,
C::SResize => S::SResize,
C::SeResize => S::SeResize,
C::SwResize => S::SwResize,
C::WResize => S::WResize,
C::EwResize => S::EwResize,
C::NsResize => S::NsResize,
C::NeswResize => S::NeswResize,
C::NwseResize => S::NwseResize,
C::ColResize => S::ColResize,
C::RowResize => S::RowResize,
C::AllScroll => S::AllScroll,
C::ZoomIn => S::ZoomIn,
C::ZoomOut => S::ZoomOut,
}
}
/// Compute the cursor shape that should be active right now and
/// push it to the compositor if it changed since the last
/// dispatch. Precedence:
///
/// 1. [`crate::App::cursor_override`] — application-driven busy
/// state, wins over everything.
/// 2. Active slider drag — `Grabbing` while the gesture machine
/// holds a `dragging_slider`.
/// 3. Cursor declared by the widget under the pointer
/// ([`crate::widget::LaidOutWidget::cursor`]).
/// 4. `Default` — pointer is over surface chrome / empty space.
pub( crate ) fn dispatch_cursor_shape( &mut self, focus: SurfaceFocus )
{
let target = {
let app_override = self.app.cursor_override();
let dragging = self.surface( focus ).gesture.dragging_slider.is_some();
let hover_cursor = self.surface( focus ).hovered_idx
.and_then( |idx| crate::tree::find_widget( &self.surface( focus ).frame.widget_rects, idx ) )
.map( |w| w.cursor )
.unwrap_or( CursorShape::Default );
let resize_cursor = self.resize_edge_under_pointer( focus ).map( resize_edge_to_cursor );
// Resize-edge hover wins over the widget cursor and over a
// `Default` app override, but loses to a non-default app
// override (so a "busy" / "wait" state still trumps chrome).
let allow_chrome = app_override.is_none()
|| app_override == Some( CursorShape::Default );
if let Some( c ) = resize_cursor.filter( |_| allow_chrome )
{
c
} else if let Some( o ) = app_override {
o
} else if dragging {
CursorShape::Grabbing
} else {
hover_cursor
}
};
// Skip when the compositor already shows the right shape.
// `current_cursor_shape == None` means "we have not pushed
// anything since the last `Enter`" — e.g. the pointer just
// arrived from another client's surface, where the prior
// client may have asked for a text I-beam. We MUST push
// unconditionally in that case so the user does not see the
// stranger cursor lingering until they hit a widget.
if self.current_cursor_shape == Some( target ) { return; }
if let Some( device ) = self.cursor_shape_device.as_ref()
{
device.set_shape( self.last_pointer_enter_serial, Self::cursor_shape_to_wp( target ) );
self.current_cursor_shape = Some( target );
}
}
/// Width (logical pixels) of the resize-grab band along each
/// edge of an xdg-toplevel surface. 6 px is the GTK / KWin
/// convention.
pub( crate ) const RESIZE_BORDER_LOGICAL: f32 = 6.0;
/// Return the [`ResizeEdge`] the pointer currently sits on, or
/// `None` if it is not in a resize-grab band. Only fires for the
/// main surface when it is an xdg-toplevel — layer-shell surfaces
/// and overlays cannot be resized by the user.
pub( crate ) fn resize_edge_under_pointer( &self, focus: SurfaceFocus ) -> Option<ResizeEdge>
{
if !matches!( focus, SurfaceFocus::Main ) { return None; }
if !matches!( self.main.surface, SurfaceKind::Window( _ ) ) { return None; }
let sf = self.surface( focus ).scale_factor.max( 1 ) as f32;
let border = Self::RESIZE_BORDER_LOGICAL * sf;
let w = self.surface( focus ).physical_width() as f32;
let h = self.surface( focus ).physical_height() as f32;
resize_edge_for_pos( self.pointer_pos, w, h, border )
}
}
#[ cfg( test ) ]
mod resize_edge_tests
{
use super::{ resize_edge_for_pos, ResizeEdge };
use crate::types::Point;
const W: f32 = 800.0;
const H: f32 = 600.0;
const B: f32 = 6.0;
fn at( x: f32, y: f32 ) -> Option<ResizeEdge>
{
resize_edge_for_pos( Point { x, y }, W, H, B )
}
#[ test ]
fn middle_is_none()
{
assert!( at( 400.0, 300.0 ).is_none() );
}
#[ test ]
fn just_inside_border_is_none()
{
// 6 px border; (6, 6) is one pixel inside on both axes.
assert!( at( 6.0, 6.0 ).is_none() );
}
#[ test ]
fn corners_take_precedence_over_single_edges()
{
assert_eq!( at( 1.0, 1.0 ), Some( ResizeEdge::TopLeft ) );
assert_eq!( at( W - 1.0, 1.0 ), Some( ResizeEdge::TopRight ) );
assert_eq!( at( 1.0, H - 1.0 ), Some( ResizeEdge::BottomLeft ) );
assert_eq!( at( W - 1.0, H - 1.0 ), Some( ResizeEdge::BottomRight ) );
}
#[ test ]
fn straight_edges()
{
assert_eq!( at( 400.0, 1.0 ), Some( ResizeEdge::Top ) );
assert_eq!( at( 400.0, H - 1.0 ), Some( ResizeEdge::Bottom ) );
assert_eq!( at( 1.0, 300.0 ), Some( ResizeEdge::Left ) );
assert_eq!( at( W - 1.0, 300.0 ), Some( ResizeEdge::Right ) );
}
}