git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/input/font_selector.rs (11.8K)

  1 use crate::colors;
  2 use crate::scene::layout::{Rect, Size};
  3 use crate::scene::paint::PaintCtx;
  4 use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
  5 use crate::widget::*;
  6 use std::sync::{Arc, Mutex};
  7 
  8 /// Font-family picker field (narrow-trait model, Phase 6as leaf sweep): click spawns
  9 /// `cce-fonts --select` and the tick reaps the child, committing its stdout as the new
 10 /// family. The detached control label rides the adapter; the model paints the field,
 11 /// the (possibly fade-truncated) family name, and the picker glyph.
 12 #[derive(Debug, Clone)]
 13 pub struct FontSelector {
 14     pub font_family: String,
 15     just_changed: bool,
 16     pressed: bool,
 17     hovered: bool,
 18     child: Arc<Mutex<Option<std::process::Child>>>,
 19     /// Raised style, the closed Dropdown's: the field is a flush inset trough
 20     /// with a transparent face (the plate shows through), the hover and press
 21     /// states a wash inside it. Defaults to `control_relief()`; the flat style
 22     /// keeps the framed dark field.
 23     raised: Option<bool>,
 24     /// Keyboard focus (FocusIn / FocusOut): lights the plate's rim and arms
 25     /// Enter / Space to open the picker.
 26     focused: bool,
 27 }
 28 
 29 impl FontSelector {
 30     /// The style in force: the per-widget override (`with_raised`) when set, else
 31     /// the DE's `control_relief`, read live so a runtime switch
 32     /// (`layout::set_control_relief`) restyles every control at once.
 33     fn raised(&self) -> bool {
 34         self.raised.unwrap_or_else(crate::layout::control_relief)
 35     }
 36 
 37     pub fn new(font_family: String) -> Adapted<FontSelector> {
 38         Adapted::new(FontSelector {
 39             font_family,
 40             just_changed: false,
 41             pressed: false,
 42             hovered: false,
 43             child: Arc::new(Mutex::new(None)),
 44             raised: None,
 45             focused: false,
 46         })
 47     }
 48 
 49     pub fn take_change(&mut self) -> bool {
 50         let changed = self.just_changed;
 51         self.just_changed = false;
 52         changed
 53     }
 54 
 55     /// The field's own labels at the laid-out rect: the family name (fade-truncated to
 56     /// fit before the picker glyph when too wide) and the glyph itself.
 57     fn field_labels(&self, rect: Rect) -> Vec<TextLabel> {
 58         let mut labels = Vec::new();
 59         let max_w = rect.width - 33.0;
 60         let full_w = TextLabel::estimate_width(&self.font_family, 12.0);
 61         let text_y = crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0);
 62 
 63         if full_w <= max_w {
 64             labels.push(TextLabel {
 65                 text: self.font_family.clone(),
 66                 x: rect.x + 8.0,
 67                 y: text_y,
 68                 font_size: 12.0,
 69                 color: [0xdd, 0xdd, 0xe2],
 70             });
 71         } else {
 72             // Find the prefix that fits in max_w - 30.0
 73             let target_prefix_w = max_w - 30.0;
 74             let mut prefix = String::new();
 75             for c in self.font_family.chars() {
 76                 let mut test_prefix = prefix.clone();
 77                 test_prefix.push(c);
 78                 if TextLabel::estimate_width(&test_prefix, 12.0) > target_prefix_w {
 79                     break;
 80                 }
 81                 prefix.push(c);
 82             }
 83 
 84             let prefix_w = TextLabel::estimate_width(&prefix, 12.0);
 85             labels.push(TextLabel {
 86                 text: prefix.clone(),
 87                 x: rect.x + 8.0,
 88                 y: text_y,
 89                 font_size: 12.0,
 90                 color: [0xdd, 0xdd, 0xe2],
 91             });
 92 
 93             // The next 5 characters fade out
 94             let remaining: Vec<char> = self.font_family.chars().skip(prefix.chars().count()).collect();
 95             let fade_colors = [
 96                 [187, 187, 193],
 97                 [154, 154, 160],
 98                 [120, 120, 128],
 99                 [87, 87, 95],
100                 [53, 53, 62],
101             ];
102             let mut cur_x = rect.x + 8.0 + prefix_w;
103             for i in 0..5 {
104                 if i < remaining.len() {
105                     let c_str = remaining[i].to_string();
106                     let c_w = TextLabel::estimate_width(&c_str, 12.0);
107                     labels.push(TextLabel {
108                         text: c_str,
109                         x: cur_x,
110                         y: text_y,
111                         font_size: 12.0,
112                         color: fade_colors[i],
113                     });
114                     cur_x += c_w;
115                 }
116             }
117         }
118 
119         // The picker glyph: "Aa", the font-picker convention, in the dropdown arrow's
120         // grey — a text glyph every face has (the emoji this drew rendered as tofu
121         // wherever no emoji font was installed).
122         labels.push(TextLabel {
123             text: "Aa".to_string(),
124             x: rect.x + rect.width - 24.0,
125             y: crate::layout::align_text_y(rect.y, rect.height, 11.0, 0.0),
126             font_size: 11.0,
127             color: [0x83, 0x83, 0x8a],
128         });
129 
130         labels
131     }
132 }
133 
134 impl Adapted<FontSelector> {
135     /// Raised style: see the `raised` field.
136     pub fn with_raised(mut self, raised: bool) -> Self {
137         self.raised = Some(raised);
138         self
139     }
140 }
141 
142 impl Layout for FontSelector {
143     fn intrinsic_size(&self) -> Option<Size> {
144         Some(Size::new(0.0, crate::layout::font_selector_height()))
145     }
146 }
147 
148 impl Paint for FontSelector {
149     fn color(&self) -> [f32; 4] {
150         // The plate shows through in both styles: a transparent-faced flush
151         // plate raised, the shared well frame flat.
152         [0.0, 0.0, 0.0, 0.0]
153     }
154 
155     fn widget_font(&self) -> Option<String> {
156         Some(crate::layout::font_selector_font())
157     }
158 
159     fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
160         let r = crate::layout::font_selector_corner_radius();
161         if r > 0.0 {
162             Some((r, (true, true, true, true)))
163         } else {
164             None
165         }
166     }
167 
168     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
169         let r = crate::layout::font_selector_corner_radius();
170         if self.raised() {
171             // The closed-dropdown chrome: a flush control plate with a
172             // transparent face, the state fill rounded to sit inside it.
173             ctx.control_plate(
174                 &crate::widget::ControlPlate::control(rect, r, crate::widget::PlateStance::Flush, None)
175                     .with_tint(self.focused.then(crate::widget::ControlPlate::focus_tint)),
176             );
177             let wash = if self.pressed {
178                 Some(colors::button_press_color())
179             } else if self.hovered {
180                 Some(colors::button_hover_color())
181             } else {
182                 None
183             };
184             if let Some(c) = wash {
185                 ctx.rounded_rect(rect, r, (true, true, true, true), c);
186             }
187             self.paint_labels(rect, ctx);
188             return;
189         }
190         // The flat style: the one well frame (`colors::well_frame_color`) over
191         // the plate, lit while pressed, rounded at the selector's radius.
192         ctx.border(rect, (r, r, r, r), [0.0; 4], colors::well_frame_color(self.hovered, self.pressed), 1.0);
193 
194         self.paint_labels(rect, ctx);
195     }
196 }
197 
198 impl FontSelector {
199     /// Labels: family text clipped short of the picker glyph (the legacy per-label
200     /// bounds), glyph unclipped.
201     fn paint_labels(&self, rect: Rect, ctx: &mut PaintCtx) {
202         let font = self.widget_font();
203         let clip_right = rect.x + rect.width - 24.0;
204         let bounds = Some([rect.x, rect.y, clip_right, rect.y + rect.height]);
205         let labels = self.field_labels(rect);
206         let count = labels.len();
207         for (idx, l) in labels.into_iter().enumerate() {
208             let b = if idx < count - 1 { bounds } else { None };
209             ctx.text_with(l.text, l.x, l.y, l.font_size, l.color, font.clone(), b);
210         }
211     }
212 }
213 
214 impl FontSelector {
215     /// Spawn `cce-fonts --select` (once — a picker already open keeps it); the
216     /// tick reaps it. The press of this plate, by pointer or by key.
217     fn open_picker(&mut self) {
218         let mut child_guard = self.child.lock().unwrap();
219         if child_guard.is_none() {
220             let home = std::env::var("HOME").unwrap_or_default();
221             let local_fonts = std::path::Path::new(&home).join(".local/bin/cce-fonts");
222             let cmd_path = if local_fonts.exists() {
223                 local_fonts.to_string_lossy().into_owned()
224             } else {
225                 "cce-fonts".to_string()
226             };
227             if let Ok(child) = std::process::Command::new(&cmd_path)
228                 .arg("--select")
229                 .arg(&self.font_family)
230                 .stdout(std::process::Stdio::piped())
231                 .spawn()
232             {
233                 *child_guard = Some(child);
234             }
235         }
236     }
237 }
238 
239 impl Input for FontSelector {
240     fn focus_role(&self) -> crate::widget::FocusRole {
241         crate::widget::FocusRole::Plate
242     }
243 
244     fn wants_tick(&self) -> bool {
245         true
246     }
247 
248     fn tick(&mut self, _dt: f32, _rect: Rect) -> bool {
249         let mut child_guard = self.child.lock().unwrap();
250         if let Some(ref mut child) = *child_guard {
251             match child.try_wait() {
252                 Ok(Some(status)) => {
253                     let child_val = child_guard.take().unwrap();
254                     if status.success() {
255                         if let Ok(output) = child_val.wait_with_output() {
256                             let stdout = String::from_utf8_lossy(&output.stdout);
257                             let trimmed = stdout.trim().to_string();
258                             if !trimmed.is_empty() && trimmed != self.font_family {
259                                 self.font_family = trimmed;
260                                 self.just_changed = true;
261                                 return true;
262                             }
263                         }
264                     }
265                 }
266                 Ok(None) => {}
267                 Err(_) => {
268                     *child_guard = None;
269                 }
270             }
271         }
272         false
273     }
274 
275     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
276         match event {
277             Event::FocusIn => {
278                 self.focused = true;
279                 false
280             }
281             Event::FocusOut => {
282                 self.focused = false;
283                 false
284             }
285             Event::KeyInput(key_event) => {
286                 // A focused plate is pressed by Enter / Space, as a Button is.
287                 if !self.focused || key_event.state != ElementState::Pressed {
288                     return false;
289                 }
290                 match key_event.logical_key {
291                     Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
292                         self.open_picker();
293                         true
294                     }
295                     _ => false,
296                 }
297             }
298             Event::MouseButton { button, state, x, y, .. } => {
299                 if *button != MouseButton::Left {
300                     return false;
301                 }
302                 match state {
303                     ElementState::Pressed => {
304                         self.pressed = true;
305                         true
306                     }
307                     ElementState::Released => {
308                         let r = ectx.rect;
309                         let inside = *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height;
310                         if self.pressed && inside {
311                             self.pressed = false;
312                             self.open_picker();
313                             return true;
314                         }
315                         let was = self.pressed;
316                         self.pressed = false;
317                         was
318                     }
319                 }
320             }
321             Event::MouseEnter => {
322                 self.hovered = true;
323                 false
324             }
325             Event::MouseLeave => {
326                 self.hovered = false;
327                 false
328             }
329             _ => false,
330         }
331     }
332 }