diff --git a/src/event_loop/context_menu.rs b/src/event_loop/context_menu.rs index 77015d9..068eb2d 100644 --- a/src/event_loop/context_menu.rs +++ b/src/event_loop/context_menu.rs @@ -92,7 +92,7 @@ impl AppData let scroll = self.text_scroll( focus, widget_idx ); let canvas = self.surface( focus ).canvas.as_ref()?; Some( crate::widget::text_edit::byte_offset_at( - canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, + canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, self.text_input_pad( focus, widget_idx ), ) ) } ); diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 823ad62..ce2b902 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -295,6 +295,7 @@ impl WindowHandler for AppData } if self.pending_size_hint_unpin { + window.set_min_size( None ); window.set_max_size( None ); self.pending_size_hint_unpin = false; } diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs index 0cd8936..da0ed5d 100644 --- a/src/event_loop/mod.rs +++ b/src/event_loop/mod.rs @@ -10,6 +10,7 @@ pub( crate ) mod drag; pub( crate ) mod focus; mod handlers; pub( crate ) mod osk; +pub( crate ) mod output_probe; pub( crate ) mod perf; pub( crate ) mod repeat; pub( crate ) mod session; diff --git a/src/event_loop/output_probe.rs b/src/event_loop/output_probe.rs new file mode 100644 index 0000000..ae39605 --- /dev/null +++ b/src/event_loop/output_probe.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +//! The screen's logical size before any surface exists, read on an event queue of its own. + +use smithay_client_toolkit::reexports::client::{ Connection, Dispatch, Proxy, QueueHandle, WEnum }; +use smithay_client_toolkit::reexports::client::globals::GlobalList; +use smithay_client_toolkit::reexports::client::protocol::wl_output::{ self, WlOutput }; +use wayland_protocols::xdg::xdg_output::zv1::client::zxdg_output_manager_v1::ZxdgOutputManagerV1; +use wayland_protocols::xdg::xdg_output::zv1::client::zxdg_output_v1::{ self, ZxdgOutputV1 }; + +struct Output +{ + mode: Option<( i32, i32 )>, + scale: i32, + logical: Option<( i32, i32 )>, +} + +struct Probe +{ + outputs: Vec, +} + +/// Logical size of the first output: `xdg_output`'s when the compositor offers it, else the current mode over the scale. +pub( crate ) fn output_logical_size( conn: &Connection, globals: &GlobalList ) -> Option<( u32, u32 )> +{ + let mut queue = conn.new_event_queue::(); + let qh = queue.handle(); + let registry = globals.registry(); + let ( outputs, manager ) = globals.contents().with_list( |list| + { + let outputs: Vec = list.iter() + .filter( |g| g.interface == WlOutput::interface().name ) + .enumerate() + .map( |( i, g )| registry.bind::( g.name, g.version.min( 4 ), &qh, i ) ) + .collect(); + let manager = list.iter() + .find( |g| g.interface == ZxdgOutputManagerV1::interface().name ) + .map( |g| registry.bind::( g.name, g.version.min( 3 ), &qh, () ) ); + ( outputs, manager ) + } ); + if outputs.is_empty() + { + return None; + } + + let mut xdg_outputs = Vec::new(); + if let Some( manager ) = &manager + { + for ( i, output ) in outputs.iter().enumerate() + { + xdg_outputs.push( manager.get_xdg_output( output, &qh, i ) ); + } + } + + let mut probe = Probe { outputs: outputs.iter().map( |_| Output { mode: None, scale: 1, logical: None } ).collect() }; + let answered = queue.roundtrip( &mut probe ).is_ok(); + + for xdg_output in xdg_outputs + { + xdg_output.destroy(); + } + if let Some( manager ) = manager + { + manager.destroy(); + } + for output in outputs + { + if output.version() >= 3 + { + output.release(); + } + } + if !answered + { + return None; + } + + probe.outputs.iter().find_map( |o| + { + let scale = o.scale.max( 1 ); + let ( w, h ) = o.logical.or_else( || o.mode.map( |( w, h )| ( w / scale, h / scale ) ) )?; + ( w > 0 && h > 0 ).then_some( ( w as u32, h as u32 ) ) + } ) +} + +impl Dispatch for Probe +{ + fn event( state: &mut Self, _: &WlOutput, event: wl_output::Event, index: &usize, _: &Connection, _: &QueueHandle ) + { + let Some( output ) = state.outputs.get_mut( *index ) else { return }; + match event + { + wl_output::Event::Mode { flags: WEnum::Value( flags ), width, height, .. } if flags.contains( wl_output::Mode::Current ) => + { + output.mode = Some( ( width, height ) ); + } + wl_output::Event::Scale { factor } => output.scale = factor, + _ => {} + } + } +} + +impl Dispatch for Probe +{ + fn event( _: &mut Self, _: &ZxdgOutputManagerV1, _: ::Event, _: &(), _: &Connection, _: &QueueHandle ) {} +} + +impl Dispatch for Probe +{ + fn event( state: &mut Self, _: &ZxdgOutputV1, event: zxdg_output_v1::Event, index: &usize, _: &Connection, _: &QueueHandle ) + { + if let ( Some( output ), zxdg_output_v1::Event::LogicalSize { width, height } ) = ( state.outputs.get_mut( *index ), event ) + { + output.logical = Some( ( width, height ) ); + } + } +} diff --git a/src/event_loop/text_editing/ime.rs b/src/event_loop/text_editing/ime.rs index 0da2dd6..e74d9c8 100644 --- a/src/event_loop/text_editing/ime.rs +++ b/src/event_loop/text_editing/ime.rs @@ -122,6 +122,16 @@ impl AppData .unwrap_or( value_handler ); Some( ( widget.rect, value, multiline, secure, align, font_size ) ) } + + /// The horizontal inner padding override of the text field at `idx`, as its hit-testing needs. + pub( crate ) fn text_input_pad( &self, focus: SurfaceFocus, idx: usize ) -> Option + { + match &find_widget( &self.surface( focus ).frame.widget_rects, idx )?.handlers + { + WidgetHandlers::TextEdit { pad_h, .. } => *pad_h, + _ => None, + } + } } /// Map a field's `secure` flag to the text-input-v3 content type. Secure diff --git a/src/event_loop/text_editing/selection.rs b/src/event_loop/text_editing/selection.rs index b19b3ed..11edeac 100644 --- a/src/event_loop/text_editing/selection.rs +++ b/src/event_loop/text_editing/selection.rs @@ -103,7 +103,7 @@ impl AppData let ss = self.surface( focus ); let canvas = match ss.canvas.as_ref() { Some( c ) => c, None => return }; crate::widget::text_edit::byte_offset_at( - canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, + canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, self.text_input_pad( focus, idx ), ) }; let ( start, end ) = word_bounds_at( &value, click_byte ); @@ -141,7 +141,7 @@ impl AppData let ss = self.surface( focus ); let canvas = match ss.canvas.as_ref() { Some( c ) => c, None => return }; crate::widget::text_edit::byte_offset_at( - canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, + canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, self.text_input_pad( focus, idx ), ) }; let ss = self.surface_mut( focus ); @@ -176,7 +176,7 @@ impl AppData let ss = self.surface( focus ); let canvas = match ss.canvas.as_ref() { Some( c ) => c, None => return }; crate::widget::text_edit::byte_offset_at( - canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, + canvas, rect, pos, &value, multiline, secure, cursor_pos, scroll, align, font_size, self.text_input_pad( focus, idx ), ) }; let ss = self.surface_mut( focus ); diff --git a/src/event_loop/window_fit.rs b/src/event_loop/window_fit.rs index 61e0b2b..7751912 100644 --- a/src/event_loop/window_fit.rs +++ b/src/event_loop/window_fit.rs @@ -13,6 +13,12 @@ use super::AppData; /// Share of an output a window may take when the compositor sent no `configure_bounds`. pub( crate ) const OUTPUT_SHARE: f32 = 0.9; +/// The part of an output of logical size `output` a window may take. +pub( crate ) fn output_share( output: ( u32, u32 ) ) -> ( u32, u32 ) +{ + ( ( output.0 as f32 * OUTPUT_SHARE ) as u32, ( output.1 as f32 * OUTPUT_SHARE ) as u32 ) +} + /// `size` scaled down, keeping its aspect ratio, until it fits in `bounds`. pub( crate ) fn fit_within( size: ( u32, u32 ), bounds: ( u32, u32 ) ) -> ( u32, u32 ) { @@ -43,8 +49,7 @@ impl AppData fn output_bounds( &self ) -> Option<( u32, u32 )> { - let ( w, h ) = self.output_logical_size( None )?; - Some( ( ( w as f32 * OUTPUT_SHARE ) as u32, ( h as f32 * OUTPUT_SHARE ) as u32 ) ) + self.output_logical_size( None ).map( output_share ) } fn output_logical_size( &self, preferred: Option<&WlOutput> ) -> Option<( u32, u32 )> diff --git a/src/input/touch/mod.rs b/src/input/touch/mod.rs index 2fbfc7a..7dcb26b 100644 --- a/src/input/touch/mod.rs +++ b/src/input/touch/mod.rs @@ -12,7 +12,8 @@ //! //! The first finger to land on a surface becomes its **primary slot** //! and drives the single-slot gesture machine (swipe, scroll, -//! long-press, drag). Any additional finger arriving while the +//! long-press, drag); like the left mouse button, it is also reported +//! through [`App::on_pointer_button`] / [`App::on_pointer_move`]. Any additional finger arriving while the //! primary is held bypasses the gesture machine and surfaces directly //! through [`App::on_touch_down`] / [`App::on_touch_move`] / //! [`App::on_touch_up`], so apps can implement pinch-zoom or @@ -153,6 +154,9 @@ impl TouchHandler for AppData } if self.osk_press( focus, pos ) { return; } + self.app.on_pointer_move( pos.x, pos.y ); + self.app.on_pointer_button( pos.x, pos.y, true ); + let outcome = { let ss = self.surface_mut( focus ); @@ -305,6 +309,7 @@ impl TouchHandler for AppData } self.pointer_pos = pp; + self.app.on_pointer_move( pp.x, pp.y ); if self.osk_move( focus, pp ) { return; } let global_drag = self.has_active_long_press_drag(); let swipe = self.swipe_config( focus ); @@ -367,6 +372,7 @@ impl AppData self.surface_mut( focus ).primary_touch_id = None; return; } + self.app.on_pointer_button( pos.x, pos.y, false ); let global_drag = self.has_active_long_press_drag(); let swipe = self.swipe_config( focus ); let events_out = diff --git a/src/widget/element.rs b/src/widget/element.rs index d6b961f..d5f7f32 100644 --- a/src/widget/element.rs +++ b/src/widget/element.rs @@ -325,6 +325,7 @@ impl Element multiline: t.is_multiline(), align: t.align, font_size: t.font_size, + pad_h: t.pad_h, select_on_focus: t.select_on_focus, password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ), } diff --git a/src/widget/handlers.rs b/src/widget/handlers.rs index aba6e19..9fd6259 100644 --- a/src/widget/handlers.rs +++ b/src/widget/handlers.rs @@ -61,6 +61,8 @@ pub enum WidgetHandlers /// drew them. `None` for fields that do not call /// `.font_size( … )` (they follow the widget-scaling mode). font_size: Option, + /// Horizontal inner padding override snapshot, for the same hit-testing. + pad_h: Option, /// `true` when the source field opted into select-all-on- /// focus. The runtime reads this in `set_focus` to decide /// whether the new selection should anchor at `0` (replace @@ -142,7 +144,7 @@ impl Clone for WidgetHandlers WidgetHandlers::Radio { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected }, WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() }, WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() }, - WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, select_on_focus, password_toggle_msg } => + WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, pad_h, select_on_focus, password_toggle_msg } => { WidgetHandlers::TextEdit { @@ -155,6 +157,7 @@ impl Clone for WidgetHandlers multiline: *multiline, align: *align, font_size: *font_size, + pad_h: *pad_h, select_on_focus: *select_on_focus, password_toggle_msg: password_toggle_msg.clone(), } diff --git a/src/widget/text_edit/draw.rs b/src/widget/text_edit/draw.rs index b81d6c4..014abd2 100644 --- a/src/widget/text_edit/draw.rs +++ b/src/widget/text_edit/draw.rs @@ -61,7 +61,7 @@ fn password_toggle_reserve( rect: Rect ) -> f32 pub fn password_toggle_hit_zone( rect: Rect ) -> Rect { let zone_w = ( password_toggle_reserve( rect ) - + single_line_pad_h( rect ) * 0.5 ).min( rect.width ); + + single_line_pad_h( rect, None ) * 0.5 ).min( rect.width ); Rect { x: rect.x + rect.width - zone_w, @@ -123,14 +123,14 @@ impl TextEdit { 0.0 }; - let pad_h = single_line_pad_h( rect ); + let pad_h = single_line_pad_h( rect, self.pad_h ); let text_rect = Rect { width: ( rect.width - toggle_reserve ).max( pad_h * 2.0 ), ..rect }; - let scroll_x = single_line_scroll_x( canvas, text_rect, &self.value, cursor_pos, secure, font_size ); - let align_x = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size ); + let scroll_x = single_line_scroll_x( canvas, text_rect, &self.value, cursor_pos, secure, font_size, self.pad_h ); + let align_x = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size, self.pad_h ); // Clip text / cursor / selection to the inner band so the // scrolled-off portion does not bleed past the pill stroke. @@ -184,7 +184,7 @@ impl TextEdit // would, so an empty centred / right-aligned field still // reads with the placeholder where the digits will land. let placeholder_align_x = single_line_align_offset( - canvas, rect, &self.placeholder, false, self.align, font_size, + canvas, rect, &self.placeholder, false, self.align, font_size, self.pad_h, ); canvas.draw_text( &self.placeholder, diff --git a/src/widget/text_edit/hit_test.rs b/src/widget/text_edit/hit_test.rs index c07879f..08eeb65 100644 --- a/src/widget/text_edit/hit_test.rs +++ b/src/widget/text_edit/hit_test.rs @@ -28,6 +28,7 @@ pub( crate ) fn byte_offset_at( scroll: TextScroll, align: crate::widget::text::TextAlign, font_size: Option, + pad_h: Option, ) -> usize { if value.is_empty() { return 0; } @@ -51,11 +52,11 @@ pub( crate ) fn byte_offset_at( // applies so a click lands on the glyph the user actually // sees, not on the byte offset that would correspond to an // unscrolled, left-aligned layout. - let scroll_x = single_line_scroll_x( canvas, rect, value, cursor_pos, secure, font_size ); - let align_x = single_line_align_offset( canvas, rect, value, secure, align, font_size ); + let scroll_x = single_line_scroll_x( canvas, rect, value, cursor_pos, secure, font_size, pad_h ); + let align_x = single_line_align_offset( canvas, rect, value, secure, align, font_size, pad_h ); byte_offset_in_line( canvas, value, 0, value.len(), - pos.x - rect.x - single_line_pad_h( rect ) - align_x + scroll_x, + pos.x - rect.x - single_line_pad_h( rect, pad_h ) - align_x + scroll_x, secure, font_size, ) @@ -86,9 +87,10 @@ pub( crate ) fn single_line_scroll_x( cursor: usize, secure: bool, font_size: f32, + pad_h: Option, ) -> f32 { - let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size ); + let inner_width = ( rect.width - single_line_pad_h( rect, pad_h ) * 2.0 ).max( font_size ); let safe_cursor = cursor.min( value.len() ); let display_value: String = if secure @@ -133,11 +135,12 @@ pub( crate ) fn single_line_align_offset( secure: bool, align: crate::widget::text::TextAlign, font_size: f32, + pad_h: Option, ) -> f32 { use crate::widget::text::TextAlign; if matches!( align, TextAlign::Left ) { return 0.0; } - let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size ); + let inner_width = ( rect.width - single_line_pad_h( rect, pad_h ) * 2.0 ).max( font_size ); let display_value: String = if secure { "\u{2022}".repeat( value.chars().count() ) diff --git a/src/widget/text_edit/metrics.rs b/src/widget/text_edit/metrics.rs index aba3c76..34c0482 100644 --- a/src/widget/text_edit/metrics.rs +++ b/src/widget/text_edit/metrics.rs @@ -12,9 +12,9 @@ pub( crate ) fn single_line_scale( rect: Rect ) -> f32 rect.height / theme::HEIGHT } -pub( crate ) fn single_line_pad_h( rect: Rect ) -> f32 +pub( crate ) fn single_line_pad_h( rect: Rect, pad_h: Option ) -> f32 { - theme::PAD_H * single_line_scale( rect ) + pad_h.unwrap_or_else( || theme::PAD_H * single_line_scale( rect ) ) } /// Resolved layout of a multiline field, shared by sizing, drawing, diff --git a/src/widget/text_edit/mod.rs b/src/widget/text_edit/mod.rs index 9fe3c33..fef3fb9 100644 --- a/src/widget/text_edit/mod.rs +++ b/src/widget/text_edit/mod.rs @@ -153,6 +153,8 @@ pub struct TextEdit /// to fit a fixed number of glyphs (date / time pickers, inline /// numeric inputs). pub( crate ) fixed_width: Option, + /// Horizontal inner padding of the single-line field in physical pixels; `None` scales the theme padding with the field height. + pub( crate ) pad_h: Option, /// Font size in pixels for the single-line draw path. `0.0` (the /// Label font size. `None` follows the process [`crate::WidgetScaling`] /// mode at the theme default (via [`crate::Canvas::font_px`]); a @@ -214,6 +216,7 @@ impl TextEdit align: super::text::TextAlign::Left, borderless: false, fixed_width: None, + pad_h: None, font_size: None, height: None, select_on_focus: false, @@ -310,6 +313,16 @@ impl TextEdit self } + /// Set the horizontal inner padding of a single-line field, in + /// physical pixels, for a host that lays out its own (`0` lets the + /// text use the whole rect). Without it the padding follows the + /// field height. + pub fn padding_h( mut self, px: f32 ) -> Self + { + self.pad_h = Some( px.max( 0.0 ) ); + self + } + /// Override the preferred width reported to the parent layout. /// Accepts logical `f32` pixels or any [`Length`] — pair a /// [`Length::fluid`] width with a fluid font size so box and glyphs @@ -508,7 +521,7 @@ impl TextEdit ) -> usize { byte_offset_at( - canvas, rect, pos, &self.value, self.is_multiline(), self.effective_secure(), cursor_pos, scroll, self.align, self.font_size, + canvas, rect, pos, &self.value, self.is_multiline(), self.effective_secure(), cursor_pos, scroll, self.align, self.font_size, self.pad_h, ) }