Skip to main content

teksilo_core/styles/
text_selection_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Tier-3 style protocol for the touch text-selection chrome — the selection
5//! handles and the magnifier that `crate::text_touch` raises.
6//!
7//! One of the few Tier-3 traits that returns **pure-data recipes only**, with
8//! no `make_*(cfg, ctx) -> WidgetId` — the others are
9//! [`ChartStyle`](crate::styles::ChartStyle) and
10//! [`GridViewStyle`](crate::styles::GridViewStyle), and for the same reason:
11//! the chrome is single-node batched paint. Here that is a disc on a stem and a
12//! framed lens whose interior is a replay of the host's own text layer. A
13//! composed subtree would buy nothing, while forcing the affordance widgets —
14//! which live in `teksilo-core` — to depend on `teksilo-widgets`.
15//!
16//! The shipped default, `RecipeTextSelectionStyle`, therefore lives in
17//! `teksilo-widgets` (`styles/recipe_text_selection_style.rs`); this crate
18//! holds the trait, the recipes and the `Rc<dyn TextSelectionStyle>` slot type.
19//!
20//! # Density
21//!
22//! A recipe is built once per density with `for_tokens(&InputTokens)`. The two
23//! dimensions are classified differently on purpose:
24//!
25//! * the **hit extent** is a [`TargetRole::Target`](teksilo_tokens::TargetRole)
26//!   and routes through [`dp`](crate::styles::density::dp), so it can only grow
27//!   with density;
28//! * the **painted diameter** is a
29//!   [`Decoration`](teksilo_tokens::TargetRole::Decoration) and is the same at
30//!   every density. It is chrome already sized to a fingertip, and target
31//!   conformance is measured on the hit rectangle, not on the paint — the rule
32//!   `docs/density-and-targets.md` states for any affordance whose hit area is
33//!   widened rather than its ink.
34
35use std::rc::Rc;
36
37use crate::styles::{RecipeColor, Theme};
38
39/// Painted geometry and colours of one selection handle.
40///
41/// `hit_size` is the extent of the handle's own node — a square centred on the
42/// anchor point — and `diameter` is the disc drawn inside it. The disc is
43/// always centred in the hit square, so a caller that changes one without the
44/// other still gets a centred handle.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct TextSelectionHandleRecipe {
47    /// Diameter of the painted disc, in dp.
48    pub diameter: f32,
49    /// Extent of the square hit rectangle, in dp. Never below
50    /// `InputTokens::min_target_conformance`.
51    pub hit_size: f32,
52    /// Width of the stem drawn from the disc to the caret it marks, in dp.
53    /// Zero draws no stem.
54    pub stem_width: f32,
55    /// Fill of the disc and the stem.
56    pub fill: RecipeColor,
57    /// Ring drawn around the disc so it stays visible over text of its own
58    /// colour. Zero `outline_width` draws none.
59    pub outline: RecipeColor,
60    /// Width of that ring, in dp.
61    pub outline_width: f32,
62}
63
64/// Painted geometry and colours of the magnifier lens.
65///
66/// The lens interior is not painted by the style: it is a replay of the host's
67/// own text layer under a transform and a clip (see
68/// [`crate::text_touch::magnifier`]). The style owns only the frame around it.
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct TextMagnifierRecipe {
71    /// Half the lens's width, in dp — the lens is `2 × radius` wide.
72    pub radius: f32,
73    /// Half the lens's height, in dp.
74    pub half_height: f32,
75    /// Magnification applied to the replayed content.
76    pub scale: f32,
77    /// How far above the contact point the lens's bottom edge sits, in dp.
78    pub rise: f32,
79    /// Corner radius of the painted frame, in dp.
80    ///
81    /// The **clip** is rectangular — see [`crate::text_touch::magnifier`] for
82    /// why, and for what shows in the corners because of it.
83    pub corner_radius: f32,
84    /// Fill painted behind the replayed content, so glyphs from the layer
85    /// beneath do not show through.
86    pub background: RecipeColor,
87    /// Frame drawn around the lens.
88    pub border: RecipeColor,
89    /// Width of that frame, in dp.
90    pub border_width: f32,
91}
92
93/// Tier-3 protocol for the touch text-selection chrome. See the module docs.
94pub trait TextSelectionStyle: 'static {
95    /// Geometry and colours of a selection handle.
96    fn handle(&self, theme: &Theme) -> TextSelectionHandleRecipe;
97    /// Geometry and colours of the magnifier frame.
98    fn magnifier(&self, theme: &Theme) -> TextMagnifierRecipe;
99}
100
101/// Shared handle type stored in
102/// [`ComponentStyleSlots::text_selection`](crate::styles::ComponentStyleSlots).
103pub type SharedTextSelectionStyle = Rc<dyn TextSelectionStyle>;