1use crate::{Axis, Event, Flags, Id, Signal, Spec, Ui, fit, px};
2use accesskit::Role;
3use draw::RasterImage;
4use draw::edit::{self, Command, Movement};
5use parley::{
6 Affinity,
7 editing::{Cursor, Selection},
8};
9use std::{hash::Hash, time::Duration};
10use winit::{
11 event::Ime,
12 keyboard::{Key, ModifiersState, NamedKey},
13 window::CursorIcon,
14};
15
16/// A labelled push button that fills with the accent tint under the pointer.
17pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {
18 let theme = &ui.theme;
19 let spec = Spec {
20 flags: Flags::CLICKABLE,
21 size: [fit(), px(theme.font_size * 2.0)],
22 text: Some(text),
23 fill: Some(theme.chip),
24 hover_fill: Some(theme.hover()),
25 hover_border: Some(theme.accent),
26 radius: 4.0,
27 pad: [theme.font_size * 0.75, 0.0],
28 center: true,
29 role: Some(Role::Button),
30 ..Spec::default()
31 };
32 ui.leaf(part, spec)
33}
34
35/// Cancel and a dialog's default button `text`, which takes no clicks while not `enabled`,
36/// in the platform's order: the default last on macOS and GNOME, first on Windows, as their
37/// own dialogs place them. The default wears the accent around it. Whether each was clicked,
38/// Cancel first.
39pub fn dialog_buttons(ui: &mut Ui, text: &str, enabled: bool) -> [bool; 2] {
40 let default = |ui: &mut Ui| {
41 let theme = &ui.theme;
42 let spec = Spec {
43 flags: if enabled {
44 Flags::CLICKABLE
45 } else {
46 Flags::default()
47 },
48 size: [fit(), px(theme.font_size * 2.0)],
49 text: Some(text),
50 color: (!enabled).then_some(theme.text_dim),
51 fill: Some(theme.chip),
52 hover_fill: enabled.then(|| theme.hover()),
53 border: enabled.then_some(theme.accent),
54 radius: 4.0,
55 pad: [theme.font_size * 0.75, 0.0],
56 center: true,
57 role: Some(Role::Button),
58 ..Spec::default()
59 };
60 let clicked = ui.leaf("default", spec).clicked;
61 if !enabled && let Some(node) = ui.access(ui.id("default")) {
62 node.set_disabled();
63 }
64 clicked
65 };
66 let windows = edit::Platform::CURRENT == edit::Platform::Windows;
67 let mut chosen = windows && default(ui);
68 let cancel = button(ui, "cancel", "Cancel").clicked;
69 if !windows {
70 chosen = default(ui);
71 }
72 [cancel, chosen]
73}
74
75/// A short tag in the accent's colour, centred in a box `height` tall, as BETA marks a
76/// feature still settling beside its name.
77pub fn badge(ui: &mut Ui, part: impl Hash, text: &str, height: f32) {
78 let theme = &ui.theme;
79 let [red, green, blue, _] = theme.accent;
80 let tall = (theme.font_size * 1.15).min(height).round();
81 let spec = Spec {
82 size: [fit(), px(tall)],
83 text: Some(text),
84 font_size: Some((theme.font_size * 0.7).round()),
85 bold: true,
86 color: Some(theme.accent),
87 fill: Some([red, green, blue, 0.16]),
88 radius: 3.0,
89 pad: [4.0, 0.0],
90 center: true,
91 ..Spec::default()
92 };
93 ui.open(
94 part,
95 Spec {
96 size: [crate::children(), px(height)],
97 pad: [0.0, ((height - tall) / 2.0).max(0.0)],
98 ..Spec::default()
99 },
100 );
101 ui.leaf("text", spec);
102 ui.close();
103}
104
105/// The width `badge` takes for `text`.
106pub fn badge_width(ui: &mut Ui, text: &str) -> f32 {
107 let size = (ui.theme.font_size * 0.7).round();
108 ui.texts.label(text, size, ui.frame).size[0] + 8.0
109}
110
111/// A check box before `label`, `checked` or not; a click on either reports for the caller
112/// to toggle.
113pub fn check_box(ui: &mut Ui, part: impl Hash, label: &str, checked: bool) -> Signal {
114 let theme = ui.theme.clone();
115 let [height, side] = [theme.font_size * 2.0, 16.0];
116 let margin = (height - side) / 2.0;
117 let id = ui.open(
118 part,
119 Spec {
120 flags: Flags::CLICKABLE,
121 size: [crate::children(), px(height)],
122 gap: 8.0,
123 role: Some(Role::CheckBox),
124 ..Spec::default()
125 },
126 );
127 if let Some(node) = ui.access(id) {
128 node.set_toggled(checked.into());
129 }
130 // A box's signal is taken once, so its hover and click are read together.
131 let signal = ui.signal(id);
132 ui.leaf(
133 "box",
134 Spec {
135 size: [px(side), px(height)],
136 inset: [0.0, margin, 0.0, margin],
137 fill: Some(if checked { theme.accent } else { theme.base }),
138 border: Some(if checked || signal.hovered {
139 theme.accent
140 } else {
141 crate::mix(theme.chip, theme.text, 0.45)
142 }),
143 radius: 3.0,
144 icon: checked.then_some(crate::popup::CHECK),
145 color: Some([1.0; 4]),
146 ..Spec::default()
147 },
148 );
149 ui.leaf(
150 "label",
151 Spec {
152 size: [fit(), px(height)],
153 text: Some(label),
154 ..Spec::default()
155 },
156 );
157 ui.close();
158 signal
159}
160
161/// How a segmented control draws its choice, as the platform draws its own.
162#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
163pub enum Segments {
164 /// A raised face sliding along a sunken track: macOS since 11, libadwaita's toggle
165 /// groups and Windows 11's segmented control.
166 #[default]
167 Track,
168 /// Buttons joined in a row, the chosen one pressed in: Mac OS X 10.6's segmented control
169 /// and Windows 7's toggle groups.
170 Joined,
171}
172
173/// `choices` side by side, equally wide, with `chosen` picked: a radio group named `label`
174/// for assistive technology. Tab enters at the choice, and arrows pick the one beside it.
175/// Returns the choice a click or the keyboard picks.
176pub fn segmented(
177 ui: &mut Ui,
178 part: impl Hash,
179 label: &str,
180 choices: &[&str],
181 chosen: usize,
182) -> Option<usize> {
183 let theme = ui.theme.clone();
184 let joined = ui.segments == Segments::Joined;
185 let dark = draw::oklab(theme.text)[0] > 0.5;
186 let [red, green, blue, _] = theme.text;
187 let ink = |alpha| [red, green, blue, alpha];
188 let height = theme.font_size * 2.0;
189 let widest = (choices.iter()).fold(0.0_f32, |widest, choice| widest.max(ui.measure(choice)[0]));
190 let width = (widest + 2.0 * theme.font_size).round();
191 // A track shows around its face; joined buttons fill their border.
192 let inset = if joined { 1.0 } else { 2.0 };
193 let button = |shade: f32| {
194 if dark {
195 crate::mix(theme.popup, theme.text, shade)
196 } else {
197 crate::mix([1.0; 4], theme.chip, 0.75 - shade * 4.0)
198 }
199 };
200 let group = ui.open(
201 part,
202 Spec {
203 size: [crate::children(), px(height)],
204 fill: Some(if joined {
205 button(0.12)
206 } else {
207 ink(if dark { 0.1 } else { 0.07 })
208 }),
209 gradient: joined.then(|| button(0.06)),
210 border: joined.then_some(theme.chip),
211 radius: if joined { 4.0 } else { 6.0 },
212 pad: [inset, inset],
213 role: Some(Role::RadioGroup),
214 ..Spec::default()
215 },
216 );
217 if let Some(node) = ui.access(group) {
218 node.set_label(label);
219 node.set_orientation(accesskit::Orientation::Horizontal);
220 }
221 let target = chosen as f32 * width;
222 let slid = if joined {
223 ui.hold(group.child("slide"), target)
224 } else {
225 ui.animate(group.child("slide"), target)
226 };
227 ui.leaf(
228 "thumb",
229 Spec {
230 flags: Flags::FLOAT,
231 size: [px(width), px(height - 2.0 * inset)],
232 position: [inset + slid, inset],
233 fill: Some(match (joined, dark) {
234 (true, _) => theme.accent,
235 (false, true) => crate::mix(theme.popup, theme.text, 0.3),
236 (false, false) => [1.0; 4],
237 }),
238 gradient: joined.then(|| crate::mix(theme.accent, [0.0, 0.0, 0.0, 1.0], 0.25)),
239 border: (!joined && !dark).then(|| ink(0.08)),
240 shadow: (!joined && !dark).then_some([0.0, 0.0, 0.0, 0.12]),
241 radius: if joined { 3.0 } else { 4.0 },
242 ..Spec::default()
243 },
244 );
245 // Rules between the choices; a track leaves them out beside its face.
246 for between in 1..choices.len() {
247 if !joined && (between == chosen || between == chosen + 1) {
248 continue;
249 }
250 let length = if joined { height - 2.0 } else { height * 0.5 };
251 ui.leaf(
252 ("rule", between),
253 Spec {
254 flags: Flags::FLOAT,
255 size: [px(1.0), px(length)],
256 position: [
257 inset + between as f32 * width - 0.5,
258 (height - length) / 2.0,
259 ],
260 fill: Some(if joined { theme.chip } else { ink(0.15) }),
261 ..Spec::default()
262 },
263 );
264 }
265 let mut picked = None;
266 for (index, choice) in choices.iter().enumerate() {
267 let on = index == chosen;
268 let id = ui.open(
269 index,
270 Spec {
271 flags: Flags::CLICKABLE,
272 size: [px(width), crate::fill()],
273 text: Some(choice),
274 color: Some(if on && joined { [1.0; 4] } else { theme.text }),
275 hover_fill: (!on).then(|| ink(0.06)),
276 radius: if joined { 3.0 } else { 4.0 },
277 center: true,
278 role: Some(Role::RadioButton),
279 ..Spec::default()
280 },
281 );
282 if let Some(node) = ui.access(id) {
283 node.set_toggled(on.into());
284 }
285 ui.close();
286 let signal = ui.signal(id);
287 // The keyboard's arrows pick the segment they move to, as a radio group's do.
288 let arrived = ui.lasted(id.child("focus"), signal.focused) == Duration::ZERO;
289 if !on && (signal.clicked || signal.focused && arrived) {
290 picked = Some(index);
291 }
292 }
293 ui.close();
294 picked
295}
296
297/// Scrollers the platform paints, fixed along the content's edges, as Mac OS X 10.6's are.
298pub struct Scrollers {
299 /// Across a scroller, in logical pixels.
300 pub thickness: f32,
301 pub paint: Box<dyn FnMut(&Scroller) -> PaintedScroller>,
302}
303
304/// A scroller as it is to be painted, in logical pixels.
305pub struct Scroller {
306 pub axis: Axis,
307 pub length: f32,
308 pub scale: f32,
309 /// Where the offset lies in its range, from 0 to 1.
310 pub value: f32,
311 /// How much of the content shows, from 0 to 1.
312 pub proportion: f32,
313 /// The part held down.
314 pub held: Option<ScrollerPart>,
315 /// Whether the window is key, which colours the knob.
316 pub active: bool,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
320pub enum ScrollerPart {
321 Decrement,
322 Increment,
323 Knob,
324 /// The track either side of the knob, which pages.
325 Slot,
326}
327
328/// A painted scroller and where its parts lie along it, from its start in logical pixels.
329#[derive(Clone)]
330pub struct PaintedScroller {
331 /// `length` by `thickness`, at the scroller's scale.
332 pub image: RasterImage,
333 pub knob: [f32; 2],
334 pub slot: [f32; 2],
335 pub decrement: [f32; 2],
336 pub increment: [f32; 2],
337}
338
339/// How long a held arrow or track waits before repeating, and then between steps.
340const REPEAT_DELAY: Duration = Duration::from_millis(300);
341const REPEAT: Duration = Duration::from_millis(50);
342
343/// A scrollbar along the far edge of the current box, for content whose scroll offset
344/// ranges over `range` while `view` of it shows, all in one unit: the platform's scroller
345/// where `Ui::scrollers` has one, otherwise a thumb in `color` over the content. With a
346/// `corner`, its far end leaves room for the other axis's scrollbar. Returns the offset
347/// the scrollbar chose.
348#[allow(clippy::too_many_arguments)]
349pub fn scrollbar(
350 ui: &mut Ui,
351 part: impl Hash,
352 axis: Axis,
353 offset: f32,
354 range: [f32; 2],
355 view: f32,
356 corner: bool,
357 color: [f32; 4],
358) -> Option<f32> {
359 if ui.scrollers.is_some() {
360 return system_scrollbar(ui, part, axis, offset, range, view, corner);
361 }
362 let along = usize::from(axis == Axis::Y);
363 let rect = ui.laid_out(ui.current())?;
364 let length = rect[along + 2] - rect[along];
365 let cross = rect[3 - along] - rect[1 - along];
366 let span = range[1] - range[0];
367 // A thumb keeps 4 from each end and the box's far edge, and is 6 across.
368 let track = length - 8.0 - if corner { 10.0 } else { 0.0 };
369 if span <= 0.0 || track <= 0.0 {
370 return None;
371 }
372 let size = (track * view / (view + span)).max(28.0).min(track);
373 let start = 4.0 + ((offset - range[0]) / span).clamp(0.0, 1.0) * (track - size);
374 let id = ui.id(&part);
375 let signal = ui.signal(id);
376 let pointer = ui.pointer().map(|pointer| pointer[along] - rect[along]);
377 if signal.pressed
378 && let Some(pointer) = pointer
379 {
380 ui.state(id).grab = pointer - start;
381 }
382 let chosen = signal.dragging.then_some(pointer).flatten().map(|pointer| {
383 let travel = (track - size).max(f32::EPSILON);
384 let fraction = ((pointer - ui.state(id).grab - 4.0) / travel).clamp(0.0, 1.0);
385 range[0] + fraction * span
386 });
387 let mut position = [0.0; 2];
388 position[along] = start;
389 position[1 - along] = cross - 10.0;
390 let mut extent = [px(6.0); 2];
391 extent[along] = px(size);
392 ui.leaf(
393 &part,
394 Spec {
395 flags: Flags::CLICKABLE | Flags::FLOAT,
396 size: extent,
397 position,
398 fill: Some(color),
399 hover_fill: Some([color[0], color[1], color[2], (color[3] * 1.6).min(1.0)]),
400 radius: 3.0,
401 cursor: Some(CursorIcon::Default),
402 ..Spec::default()
403 },
404 );
405 chosen
406}
407
408fn system_scrollbar(
409 ui: &mut Ui,
410 part: impl Hash,
411 axis: Axis,
412 offset: f32,
413 range: [f32; 2],
414 view: f32,
415 corner: bool,
416) -> Option<f32> {
417 let along = usize::from(axis == Axis::Y);
418 let rect = ui.laid_out(ui.current())?;
419 let span = range[1] - range[0];
420 let thickness = ui.scrollers.as_ref()?.thickness;
421 let length = rect[along + 2] - rect[along] - if corner { thickness } else { 0.0 };
422 if span <= 0.0 || length <= 0.0 {
423 return None;
424 }
425 let id = ui.id(&part);
426 let signal = ui.signal(id);
427 let pointer = ui.pointer().map(|pointer| pointer[along] - rect[along]);
428 let now = ui.now;
429 if !signal.pressed && !signal.dragging {
430 ui.state(id).held = None;
431 }
432 let request = Scroller {
433 axis,
434 length,
435 scale: ui.scale,
436 value: ((offset - range[0]) / span).clamp(0.0, 1.0),
437 proportion: (view / (view + span)).clamp(0.0, 1.0),
438 held: ui.state(id).held.map(|(part, _)| part),
439 active: ui.window_focused,
440 };
441 let painted = (ui.scrollers.as_mut()?.paint)(&request);
442 let within = |[start, end]: [f32; 2], at: f32| start <= at && at < end;
443 let line = (view / 20.0).max(1.0);
444 let mut chosen = None;
445 if let Some(at) = pointer
446 && (signal.pressed || signal.dragging)
447 {
448 if signal.pressed {
449 let hit = [
450 (ScrollerPart::Decrement, painted.decrement),
451 (ScrollerPart::Increment, painted.increment),
452 (ScrollerPart::Knob, painted.knob),
453 (ScrollerPart::Slot, painted.slot),
454 ]
455 .into_iter()
456 .find(|(_, extent)| within(*extent, at));
457 ui.state(id).held = hit.map(|(part, _)| (part, now));
458 ui.state(id).grab = at - painted.knob[0];
459 }
460 match ui.state(id).held {
461 Some((ScrollerPart::Knob, _)) => {
462 let travel =
463 (painted.slot[1] - painted.slot[0]) - (painted.knob[1] - painted.knob[0]);
464 let from = at - ui.state(id).grab - painted.slot[0];
465 let fraction = (from / travel.max(f32::EPSILON)).clamp(0.0, 1.0);
466 chosen = Some(range[0] + fraction * span);
467 }
468 Some((part, due)) if now >= due => {
469 let step = match part {
470 ScrollerPart::Decrement => -line,
471 ScrollerPart::Increment => line,
472 // The track pages towards the pointer and stops once the knob is under it.
473 _ if within(painted.knob, at) => 0.0,
474 _ if at < painted.knob[0] => line - view,
475 _ => view - line,
476 };
477 let delay = if signal.pressed { REPEAT_DELAY } else { REPEAT };
478 ui.state(id).held = Some((part, now + delay));
479 chosen = Some((offset + step).clamp(range[0], range[1]));
480 }
481 _ => {}
482 }
483 // A held part repeats on later frames.
484 ui.animating = true;
485 }
486 let mut position = [0.0; 2];
487 position[1 - along] = rect[3 - along] - rect[1 - along] - thickness;
488 let mut extent = [px(thickness); 2];
489 extent[along] = px(length);
490 ui.leaf(
491 &part,
492 Spec {
493 flags: Flags::CLICKABLE | Flags::FLOAT,
494 size: extent,
495 position,
496 image: Some(&painted.image),
497 cursor: Some(CursorIcon::Default),
498 ..Spec::default()
499 },
500 );
501 if axis == Axis::Y && corner {
502 // The corner between the two scrollers shows the content's white, as AppKit's does.
503 ui.leaf(
504 ("corner", &part),
505 Spec {
506 flags: Flags::FLOAT,
507 size: [px(thickness); 2],
508 position: [position[0], length],
509 fill: Some([1.0; 4]),
510 ..Spec::default()
511 },
512 );
513 }
514 chosen
515}
516
517/// A single-line field editing `text`, showing `placeholder` while empty. Presses, drags
518/// and keys select and edit as on the page: a double press selects a word, a triple press
519/// everything, and dragging extends by that unit.
520pub fn text_field(
521 ui: &mut Ui,
522 id: Id,
523 text: &mut String,
524 placeholder: &str,
525 spec: Spec<'_>,
526) -> Signal {
527 field(ui, id, text, placeholder, spec, false)
528}
529
530/// A text field for a password, showing and announcing a bullet for each character.
531pub fn password_field(
532 ui: &mut Ui,
533 id: Id,
534 text: &mut String,
535 placeholder: &str,
536 spec: Spec<'_>,
537) -> Signal {
538 field(ui, id, text, placeholder, spec, true)
539}
540
541fn field(
542 ui: &mut Ui,
543 id: Id,
544 text: &mut String,
545 placeholder: &str,
546 spec: Spec<'_>,
547 secret: bool,
548) -> Signal {
549 const BULLET: &str = "\u{2022}";
550 // The text laid out, and where its offsets fall in `text`: a secret shows bullets.
551 let masked = |text: &str| {
552 if secret {
553 BULLET.repeat(text.chars().count())
554 } else {
555 text.to_owned()
556 }
557 };
558 let to_text = |text: &str, index: usize| match secret {
559 true => (text.char_indices().map(|(at, _)| at))
560 .nth(index / BULLET.len())
561 .unwrap_or(text.len()),
562 false => index,
563 };
564 let to_shown = |text: &str, index: usize| match secret {
565 true => text[..index].chars().count() * BULLET.len(),
566 false => index,
567 };
568 let mut shown_text = masked(text);
569 let signal = ui.signal(id);
570 let pad = spec.pad[0];
571 let size = ui.theme.font_size;
572 let modifiers = edit_modifiers(ui.modifiers());
573 let pointer = ui
574 .pointer()
575 .zip(ui.laid_out(id))
576 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)
577 .filter(|_| signal.pressed || signal.dragging);
578 let state = ui.state(id);
579 let (mut selection, mut press, select) = (state.selection, state.press, state.select.take());
580 let before = [selection.anchor(), selection.focus()];
581 let (texts, frame) = (&mut ui.texts, ui.frame);
582 let mut label = texts.label(&shown_text, size, frame);
583 if let Some(x) = pointer {
584 let layout = &label.layout;
585 let unit = if signal.pressed { signal.unit } else { press.1 };
586 let hit = Cursor::from_point(layout, x, label.size[1] / 2.0);
587 let target = edit::selection_at(layout, hit, x, unit);
588 selection = if signal.pressed {
589 let pressed = if modifiers.shift {
590 Selection::new(selection.anchor(), target.focus())
591 } else {
592 target
593 };
594 press = (pressed, unit);
595 pressed
596 } else {
597 let [anchor, focus] = edit::drag(
598 [press.0.anchor(), press.0.focus()],
599 [target.anchor(), target.focus()],
600 unit,
601 |cursor| cursor.index(),
602 );
603 Selection::new(anchor, focus)
604 };
605 }
606 selection = if let Some([anchor, focus]) = select {
607 Selection::new(
608 Cursor::from_byte_index(&label.layout, anchor, Affinity::Downstream),
609 Cursor::from_byte_index(&label.layout, focus, Affinity::Upstream),
610 )
611 } else {
612 selection.refresh(&label.layout)
613 };
614 for event in &signal.events {
615 let layout = &label.layout;
616 let (range, inserted) = match event {
617 Event::Ime(Ime::Commit(committed)) => (selection.text_range(), committed.as_str()),
618 Event::Key { key, text: typed } => {
619 let key = edit_key(key);
620 // One line has no break for Control-K to join, nor room for Home, End and the
621 // page keys to scroll.
622 let command = match (Command::from_key(&key, modifiers), &key) {
623 (Some(Command::Kill), _) => Some(Command::DeleteTo(Movement::LineEnd)),
624 (
625 Some(
626 Command::ScrollPage { .. }
627 | Command::MovePage { .. }
628 | Command::MoveParagraphs { .. },
629 ),
630 _,
631 ) => None,
632 (None, edit::Key::Named(edit::NamedKey::Home)) => {
633 Some(Command::Move(Movement::DocumentStart))
634 }
635 (None, edit::Key::Named(edit::NamedKey::End)) => {
636 Some(Command::Move(Movement::DocumentEnd))
637 }
638 (command, _) => command,
639 };
640 let collapsed = selection.is_collapsed();
641 match (command, key) {
642 (Some(Command::Move(movement)), _) => {
643 selection = edit::step(layout, selection, movement, modifiers.shift);
644 continue;
645 }
646 (Some(Command::DeleteTo(movement)), _) if collapsed => (
647 edit::step(layout, selection, movement, true).text_range(),
648 "",
649 ),
650 (Some(Command::Delete { backward }), _) if collapsed => {
651 let caret = selection.focus();
652 let cluster = caret.logical_clusters(layout)[usize::from(!backward)];
653 (
654 cluster.map_or(caret.index()..caret.index(), |cluster| {
655 cluster.text_range()
656 }),
657 "",
658 )
659 }
660 (Some(_), _) => (selection.text_range(), ""),
661 (None, edit::Key::Character(character))
662 if modifiers.command && character.eq_ignore_ascii_case("a") =>
663 {
664 selection = Selection::new(
665 Cursor::from_byte_index(layout, 0, Affinity::Downstream),
666 Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream),
667 );
668 continue;
669 }
670 (None, _) => match typed.as_deref().filter(|typed| {
671 !modifiers.command
672 && !modifiers.control
673 && !typed.is_empty()
674 && !typed.chars().any(char::is_control)
675 }) {
676 Some(typed) => (selection.text_range(), typed),
677 None => continue,
678 },
679 }
680 }
681 _ => continue,
682 };
683 let range = to_text(text, range.start)..to_text(text, range.end);
684 text.replace_range(range.clone(), inserted);
685 shown_text = masked(text);
686 label = texts.label(&shown_text, size, frame);
687 selection = Selection::from_byte_index(
688 &label.layout,
689 to_shown(text, range.start + inserted.len()),
690 Affinity::Downstream,
691 );
692 }
693 let moved = signal.pressed || before != [selection.anchor(), selection.focus()];
694 let state = ui.state(id);
695 state.selection = selection;
696 state.press = press;
697 let theme = ui.theme.clone();
698 let shown = if text.is_empty() {
699 placeholder
700 } else {
701 shown_text.as_str()
702 };
703 let height = spec.size[1];
704 let backdrop = spec.fill.unwrap_or(theme.base);
705 ui.open_as(
706 id,
707 Spec {
708 flags: spec.flags | Flags::CLICKABLE | Flags::FOCUSABLE,
709 text: Some(shown),
710 color: Some(if text.is_empty() {
711 theme.text_dim
712 } else {
713 theme.text
714 }),
715 cursor: Some(CursorIcon::Text),
716 role: spec.role.or(Some(if secret {
717 Role::PasswordInput
718 } else {
719 Role::TextInput
720 })),
721 ..spec
722 },
723 );
724 if let Some(node) = ui.access(id) {
725 node.set_value(shown_text.as_str());
726 if !placeholder.is_empty() {
727 node.set_placeholder(placeholder);
728 }
729 }
730 if signal.focused {
731 let line = label.size[1];
732 let top = match height.size {
733 crate::Size::Pixels(height) => (height - line) / 2.0,
734 _ => 0.0,
735 };
736 let fill = if ui.window_focused {
737 theme.selection
738 } else {
739 theme.inactive_selection
740 };
741 for (rect, _) in selection.geometry(&label.layout) {
742 ui.mark(
743 [pad + rect.x0 as f32, top, pad + rect.x1 as f32, top + line],
744 fill,
745 0.0,
746 );
747 }
748 if ui.window_focused && selection.is_collapsed() {
749 let opacity = ui.blink(id, moved);
750 let x = pad + selection.focus().geometry(&label.layout, 0.0).x0 as f32;
751 let half = edit::CARET_WIDTH / 2.0;
752 ui.mark(
753 [x - half, top, x + half, top + line],
754 edit::caret_color(theme.caret, backdrop, opacity),
755 half,
756 );
757 }
758 }
759 ui.close();
760 signal
761}
762
763/// A winit key in the editing vocabulary text fields and the page share.
764pub fn edit_key(key: &Key) -> edit::Key {
765 use edit::NamedKey as Edit;
766 match key {
767 Key::Character(text) => edit::Key::Character(text.to_string()),
768 Key::Named(named) => edit::Key::Named(match named {
769 NamedKey::Escape => Edit::Escape,
770 NamedKey::Tab => Edit::Tab,
771 NamedKey::Space => Edit::Space,
772 NamedKey::Enter => Edit::Enter,
773 NamedKey::Backspace => Edit::Backspace,
774 NamedKey::Delete => Edit::Delete,
775 NamedKey::ArrowLeft => Edit::ArrowLeft,
776 NamedKey::ArrowRight => Edit::ArrowRight,
777 NamedKey::ArrowUp => Edit::ArrowUp,
778 NamedKey::ArrowDown => Edit::ArrowDown,
779 NamedKey::Home => Edit::Home,
780 NamedKey::End => Edit::End,
781 NamedKey::PageUp => Edit::PageUp,
782 NamedKey::PageDown => Edit::PageDown,
783 NamedKey::F7 => Edit::F7,
784 NamedKey::F11 => Edit::F11,
785 NamedKey::Alt
786 | NamedKey::AltGraph
787 | NamedKey::Control
788 | NamedKey::Shift
789 | NamedKey::Super
790 | NamedKey::Meta => Edit::Modifier,
791 _ => Edit::Other,
792 }),
793 _ => edit::Key::Named(Edit::Other),
794 }
795}
796
797pub fn edit_modifiers(modifiers: ModifiersState) -> edit::Modifiers {
798 edit::Modifiers {
799 shift: modifiers.shift_key(),
800 control: modifiers.control_key(),
801 option: modifiers.alt_key(),
802 command: if edit::Platform::CURRENT == edit::Platform::MacOs {
803 modifiers.super_key()
804 } else {
805 modifiers.control_key()
806 },
807 }
808}