teksilo_widgets/styles/
recipe_text_selection_style.rs1use teksilo_core::styles::density::dp;
19use teksilo_core::styles::{
20 RecipeColor, TextMagnifierRecipe, TextSelectionHandleRecipe, TextSelectionStyle, Theme,
21};
22use teksilo_core::text_touch::{HANDLE_DIAMETER, HANDLE_HIT_SIZE, HANDLE_STEM_WIDTH, magnifier};
23use teksilo_tokens::{BorderRole, InputTokens, SurfaceRole, TargetRole};
24
25pub const HANDLE_OUTLINE_WIDTH: f32 = 1.0;
27
28pub const MAGNIFIER_BORDER_WIDTH: f32 = 1.0;
30
31pub const MAGNIFIER_CORNER_RADIUS: f32 = 0.0;
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct TextSelectionRecipe {
46 pub handle_diameter: f32,
47 pub handle_hit_size: f32,
48 pub handle_stem_width: f32,
49 pub handle_outline_width: f32,
50 pub magnifier_radius: f32,
51 pub magnifier_half_height: f32,
52 pub magnifier_scale: f32,
53 pub magnifier_rise: f32,
54 pub magnifier_corner_radius: f32,
55 pub magnifier_border_width: f32,
56}
57
58impl TextSelectionRecipe {
59 pub fn for_tokens(tokens: &InputTokens) -> Self {
69 Self {
70 handle_diameter: dp(HANDLE_DIAMETER, TargetRole::Decoration, tokens),
71 handle_hit_size: dp(HANDLE_HIT_SIZE, TargetRole::Target, tokens),
72 handle_stem_width: HANDLE_STEM_WIDTH,
73 handle_outline_width: HANDLE_OUTLINE_WIDTH,
74 magnifier_radius: magnifier::MAGNIFIER_RADIUS,
75 magnifier_half_height: magnifier::MAGNIFIER_HALF_HEIGHT,
76 magnifier_scale: magnifier::MAGNIFIER_SCALE,
77 magnifier_rise: magnifier::MAGNIFIER_RISE,
78 magnifier_corner_radius: MAGNIFIER_CORNER_RADIUS,
79 magnifier_border_width: MAGNIFIER_BORDER_WIDTH,
80 }
81 }
82}
83
84impl Default for TextSelectionRecipe {
85 fn default() -> Self {
86 Self::for_tokens(&InputTokens::default())
87 }
88}
89
90#[derive(Debug, Default, Clone, Copy)]
93pub struct RecipeTextSelectionStyle {
94 pub recipe: TextSelectionRecipe,
95}
96
97impl RecipeTextSelectionStyle {
98 pub fn new(recipe: TextSelectionRecipe) -> Self {
99 Self { recipe }
100 }
101
102 pub fn for_tokens(tokens: &InputTokens) -> Self {
106 Self {
107 recipe: TextSelectionRecipe::for_tokens(tokens),
108 }
109 }
110}
111
112impl TextSelectionStyle for RecipeTextSelectionStyle {
113 fn handle(&self, _theme: &Theme) -> TextSelectionHandleRecipe {
114 TextSelectionHandleRecipe {
115 diameter: self.recipe.handle_diameter,
116 hit_size: self.recipe.handle_hit_size,
117 stem_width: self.recipe.handle_stem_width,
118 fill: RecipeColor::Surface(SurfaceRole::Accent),
119 outline: RecipeColor::Surface(SurfaceRole::EditorBg),
123 outline_width: self.recipe.handle_outline_width,
124 }
125 }
126
127 fn magnifier(&self, _theme: &Theme) -> TextMagnifierRecipe {
128 TextMagnifierRecipe {
129 radius: self.recipe.magnifier_radius,
130 half_height: self.recipe.magnifier_half_height,
131 scale: self.recipe.magnifier_scale,
132 rise: self.recipe.magnifier_rise,
133 corner_radius: self.recipe.magnifier_corner_radius,
134 background: RecipeColor::Surface(SurfaceRole::EditorBg),
135 border: RecipeColor::Border(BorderRole::Default),
136 border_width: self.recipe.magnifier_border_width,
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use teksilo_tokens::TargetDensity;
145
146 #[test]
149 fn the_default_recipe_is_the_compact_projection() {
150 assert_eq!(
151 TextSelectionRecipe::default(),
152 TextSelectionRecipe::for_tokens(&InputTokens::for_density(TargetDensity::Compact))
153 );
154 assert_eq!(
155 TextSelectionRecipe::default().handle_diameter,
156 HANDLE_DIAMETER
157 );
158 assert_eq!(
159 TextSelectionRecipe::default().handle_hit_size,
160 HANDLE_HIT_SIZE
161 );
162 }
163
164 #[test]
167 fn the_controllers_metrics_come_from_this_recipe() {
168 for density in [
169 TargetDensity::Compact,
170 TargetDensity::Comfortable,
171 TargetDensity::Touch,
172 ] {
173 let tokens = InputTokens::for_density(density);
174 let style = RecipeTextSelectionStyle::for_tokens(&tokens);
175 let recipe = style.handle(&teksilo_core::presets::intui::light());
176 let metrics = teksilo_core::text_touch::HandleMetrics::from(&recipe);
177 assert_eq!(
178 metrics,
179 teksilo_core::text_touch::HandleMetrics::for_tokens(&tokens),
180 "{density:?}"
181 );
182 }
183 }
184
185 #[test]
189 fn the_shipped_lens_frame_matches_the_rectangular_clip() {
190 assert_eq!(TextSelectionRecipe::default().magnifier_corner_radius, 0.0);
191 }
192
193 #[test]
196 fn a_handles_target_clears_the_conformance_floor_at_every_density() {
197 for density in [
198 TargetDensity::Compact,
199 TargetDensity::Comfortable,
200 TargetDensity::Touch,
201 ] {
202 let tokens = InputTokens::for_density(density);
203 let recipe = TextSelectionRecipe::for_tokens(&tokens);
204 assert!(
205 recipe.handle_hit_size >= tokens.min_target_conformance,
206 "{density:?}: {} dp target",
207 recipe.handle_hit_size
208 );
209 }
210 }
211}