Unify TextEdit's font size with the button label. The button resolved its `font_size` Length in font space (`viewport_logical`, so the `× dpi_scale` at raster lands the right physical size), while `TextEdit::font_size` took a raw `f32` that the draw / hit-test paths treated as a logical size. A caller that resolved a Length against the physical surface and passed the result as that `f32` therefore double-counted `dpi_scale` and got a font that rendered too large. `TextEdit::font_size` now takes `impl Into<Length>` and is resolved against `viewport_logical` exactly like the button, so the two paths agree. The field stores `Option<Length>` (`None` follows the widget-scaling mode at the theme default), retiring the old `f32` sentinel (`0.0` = mode, negative = fluid design px). `font_size_fluid` becomes shorthand for `Length::fluid( n )`. The `Option<Length>` flows through the `WidgetHandlers::TextEdit` snapshot and `text_input_geometry` and is resolved at draw / hit-test time, both of which carry a canvas; the inner measure helpers (`wrapping`, `hit_test`) keep `f32` because they receive the already-resolved value. Backward compatible: `f32` call sites still compile via `f32: Into<Length>` (→ `Length::px`), with the same result as before. Add `Button::width( impl Into<Length> )`. Text buttons size to their label plus padding; some layouts need a pinned width instead — a full-width or surface-proportional button. The new builder mirrors `height`: resolved in physical layout space, clamped to the available `max_width`, propagated through `map_msg`, and a no-op for icon buttons. Add `Text::line_height( mult )`. Wrapped multi-line text used the font's declared leading (`new_line_size`), which is tight for some labels; the multiplier scales the gap between wrapped lines (`1.0`, the default, keeps the natural leading — every other `text` is unchanged — and a `0.5` floor keeps lines from overlapping). Applied uniformly in `preferred_size` and `draw` so the reported height and the drawn baselines stay consistent. Tests: `button` gains a pinned-width and a size-to-content case; `text` gains a line-height default / clamp test and a check that doubling the line height doubles a wrapped block's reported height. Docs: `docs/widgets.md` `text` / `button` / `separator` sections updated for the new builders, and a `CHANGELOG.md` "Unreleased" section covering this batch alongside the responsive work already landed.
112 lines
3.5 KiB
Rust
112 lines
3.5 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
use smithay_client_toolkit::reexports::client::QueueHandle;
|
|
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3;
|
|
|
|
use crate::app::App;
|
|
use crate::event_loop::app_data::AppData;
|
|
use crate::event_loop::surface::SurfaceFocus;
|
|
use crate::tree::find_widget;
|
|
use crate::types::{ Length, Rect };
|
|
use crate::widget::WidgetHandlers;
|
|
|
|
impl<A: App> AppData<A>
|
|
{
|
|
pub( crate ) fn activate_text_input( &mut self, qh: &QueueHandle<Self>, secure: bool )
|
|
{
|
|
self.text_input_secure = secure;
|
|
let ( hint, purpose ) = content_type( secure );
|
|
match ( &self.text_input_manager, &self.text_input )
|
|
{
|
|
( Some( manager ), None ) =>
|
|
{
|
|
let seats: Vec<_> = self.seat_state.seats().collect();
|
|
if let Some( seat ) = seats.into_iter().next()
|
|
{
|
|
let ti = manager.get_text_input( &seat, qh, () );
|
|
ti.enable();
|
|
ti.set_content_type( hint, purpose );
|
|
ti.commit();
|
|
self.text_input = Some( ti );
|
|
} else {
|
|
eprintln!( "ltk: activate_text_input: no seat available" );
|
|
}
|
|
}
|
|
( None, _ ) =>
|
|
eprintln!( "ltk: activate_text_input: no text_input_manager (compositor did not advertise zwp_text_input_manager_v3)" ),
|
|
// Focus moved between text fields: refresh the content type (e.g.
|
|
// flag a password field) without re-creating the object.
|
|
( Some( _ ), Some( ti ) ) =>
|
|
{
|
|
ti.set_content_type( hint, purpose );
|
|
ti.commit();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub( crate ) fn deactivate_text_input( &mut self )
|
|
{
|
|
if let Some( ti ) = self.text_input.take()
|
|
{
|
|
ti.disable();
|
|
ti.commit();
|
|
ti.destroy();
|
|
}
|
|
}
|
|
|
|
pub( crate ) fn reenable_text_input( &self )
|
|
{
|
|
if let Some( ti ) = &self.text_input
|
|
{
|
|
let ( hint, purpose ) = content_type( self.text_input_secure );
|
|
ti.enable();
|
|
ti.set_content_type( hint, purpose );
|
|
ti.commit();
|
|
}
|
|
}
|
|
|
|
/// Snapshot a focused-widget's geometry needed by the pointer
|
|
/// hit-testers for text editing. Returns `None` when the widget
|
|
/// isn't a TextEdit or its rect is missing — the helper above
|
|
/// short-circuits in that case.
|
|
pub( crate ) fn text_input_geometry(
|
|
&self,
|
|
focus: SurfaceFocus,
|
|
idx: usize,
|
|
) -> Option<( Rect, String, bool, bool, crate::widget::text::TextAlign, Option<Length> )>
|
|
{
|
|
let ss = self.surface( focus );
|
|
let widget = find_widget( &ss.frame.widget_rects, idx )?;
|
|
let ( value_handler, multiline, secure, align, font_size ) = match &widget.handlers
|
|
{
|
|
WidgetHandlers::TextEdit { value, multiline, secure, align, font_size, .. } =>
|
|
( value.clone(), *multiline, *secure, *align, *font_size ),
|
|
_ => return None,
|
|
};
|
|
// Prefer the *pending* value (typed-but-not-yet-applied) when
|
|
// it exists — that's what the user sees right now and what
|
|
// the cursor measurements should be relative to.
|
|
let value = ss.pending_text_values.get( &idx )
|
|
.cloned()
|
|
.unwrap_or( value_handler );
|
|
Some( ( widget.rect, value, multiline, secure, align, font_size ) )
|
|
}
|
|
}
|
|
|
|
/// Map a field's `secure` flag to the text-input-v3 content type. Secure
|
|
/// fields are flagged `Password` with `SensitiveData | HiddenText` so the
|
|
/// IME/OSK skips prediction, autocorrect and storing the value.
|
|
fn content_type( secure: bool ) -> ( zwp_text_input_v3::ContentHint, zwp_text_input_v3::ContentPurpose )
|
|
{
|
|
if secure
|
|
{
|
|
(
|
|
zwp_text_input_v3::ContentHint::SensitiveData | zwp_text_input_v3::ContentHint::HiddenText,
|
|
zwp_text_input_v3::ContentPurpose::Password,
|
|
)
|
|
} else {
|
|
( zwp_text_input_v3::ContentHint::None, zwp_text_input_v3::ContentPurpose::Normal )
|
|
}
|
|
}
|