1use frus_core::{BorderRadius, Rect, Scene, ShapeBorder, Size};
25use frus_layout::{Align, Dimension, FlexDirection, Style};
26
27use crate::interaction::Status;
28use crate::theme::Theme;
29use crate::widget::Widget;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33pub enum EditAction {
34 Cut,
36 Copy,
38 Paste,
40 SelectAll,
42}
43
44impl EditAction {
45 pub fn label(self) -> &'static str {
47 match self {
48 EditAction::Cut => "Cut",
49 EditAction::Copy => "Copy",
50 EditAction::Paste => "Paste",
51 EditAction::SelectAll => "Select all",
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
59pub struct ToolbarContext {
60 pub has_selection: bool,
62 pub can_paste: bool,
65 pub all_selected: bool,
68}
69
70impl ToolbarContext {
71 pub(crate) const VARIANTS: u8 = 8;
73
74 pub(crate) fn variant(self) -> u8 {
77 u8::from(self.has_selection)
78 | u8::from(self.can_paste) << 1
79 | u8::from(self.all_selected) << 2
80 }
81}
82
83#[derive(Clone)]
85pub struct ToolbarItem<Msg = crate::callback::Callback> {
86 kind: ItemKind<Msg>,
87}
88
89#[derive(Clone)]
90enum ItemKind<Msg> {
91 Action(EditAction),
92 Custom { label: String, message: Msg },
93}
94
95impl<Msg> ToolbarItem<Msg> {
96 pub fn action(action: EditAction) -> Self {
98 Self {
99 kind: ItemKind::Action(action),
100 }
101 }
102
103 pub fn cut() -> Self {
105 Self::action(EditAction::Cut)
106 }
107
108 pub fn copy() -> Self {
110 Self::action(EditAction::Copy)
111 }
112
113 pub fn paste() -> Self {
115 Self::action(EditAction::Paste)
116 }
117
118 pub fn select_all() -> Self {
120 Self::action(EditAction::SelectAll)
121 }
122
123 pub fn custom(label: impl Into<String>, message: Msg) -> Self {
126 Self {
127 kind: ItemKind::Custom {
128 label: label.into(),
129 message,
130 },
131 }
132 }
133
134 pub fn label(&self) -> &str {
136 match &self.kind {
137 ItemKind::Action(action) => action.label(),
138 ItemKind::Custom { label, .. } => label,
139 }
140 }
141
142 pub fn edit_action(&self) -> Option<EditAction> {
144 match &self.kind {
145 ItemKind::Action(action) => Some(*action),
146 ItemKind::Custom { .. } => None,
147 }
148 }
149}
150
151const HEIGHT: f32 = 44.0;
153const PAD_END: f32 = 14.5;
155const PAD_MIDDLE: f32 = 9.5;
157const MIN_WIDTH: f32 = 48.0;
159const ELEVATION: f32 = 1.0;
162
163fn colours(theme: &Theme) -> (frus_core::Color, frus_core::Color) {
168 let dark = theme.scheme.brightness == crate::Brightness::Dark;
169 let default = if dark { Theme::dark() } else { Theme::light() };
170 if theme.scheme.surface == default.scheme.surface
171 && theme.scheme.on_surface == default.scheme.on_surface
172 {
173 if dark {
174 (
175 frus_core::Color::rgb(
176 0x42 as f32 / 255.0,
177 0x42 as f32 / 255.0,
178 0x42 as f32 / 255.0,
179 ),
180 frus_core::Color::WHITE,
181 )
182 } else {
183 (frus_core::Color::WHITE, frus_core::Color::BLACK)
184 }
185 } else {
186 (theme.scheme.surface, theme.scheme.on_surface)
187 }
188}
189
190pub(crate) fn uses_menu(platform: frus_core::TargetPlatform) -> bool {
195 use frus_core::TargetPlatform as P;
196 match platform {
197 P::Fuchsia | P::Linux | P::Windows => true,
198 P::Android | P::Ios | P::MacOs => false,
199 }
200}
201
202const MENU_WIDTH: f32 = 222.0;
204const MENU_RADIUS: f32 = 7.0;
206const MENU_SCREEN_PADDING: f32 = 8.0;
208const ROW_HEIGHT: f32 = 36.0;
210const ROW_PAD_X: f32 = 20.0;
212const ROW_PAD_BOTTOM: f32 = 3.0;
213
214pub(crate) struct SelectionToolbar<Msg> {
217 pill: Vec<Box<dyn Widget<Msg>>>,
218 menu: Vec<Box<dyn Widget<Msg>>>,
219 desktop: std::cell::Cell<bool>,
222}
223
224impl<Msg: Clone + 'static> SelectionToolbar<Msg> {
225 pub(crate) fn new(items: Vec<ToolbarItem<Msg>>) -> Self {
227 let total = items.len();
228 let pill = items
229 .iter()
230 .cloned()
231 .enumerate()
232 .map(|(index, item)| {
233 let button: Box<dyn Widget<Msg>> =
234 Box::new(ToolbarButton::new(item, index == 0, index + 1 == total));
235 button
236 })
237 .collect();
238 let menu = items
239 .into_iter()
240 .map(|item| Box::new(MenuButton::new(item)) as Box<dyn Widget<Msg>>)
241 .collect();
242 Self {
243 pill,
244 menu,
245 desktop: std::cell::Cell::new(false),
246 }
247 }
248}
249
250impl<Msg: Clone> Widget<Msg> for SelectionToolbar<Msg> {
251 fn style_themed(&self, theme: &Theme) -> Style {
252 let desktop = uses_menu(theme.platform);
253 self.desktop.set(desktop);
254 if desktop {
255 Style {
256 width: Dimension::Length(MENU_WIDTH),
257 height: Dimension::Auto,
258 flex_direction: FlexDirection::Column,
259 ..Default::default()
260 }
261 } else {
262 Widget::<Msg>::style(self)
263 }
264 }
265
266 fn style(&self) -> Style {
267 Style {
268 width: Dimension::Auto,
269 height: Dimension::Length(HEIGHT),
270 flex_direction: FlexDirection::Row,
271 align: Align::Center,
272 ..Default::default()
273 }
274 }
275
276 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
277 if self.desktop.get() {
278 &self.menu
279 } else {
280 &self.pill
281 }
282 }
283
284 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
285 let o = status.opacity;
286 if self.desktop.get() {
287 let radius = BorderRadius::uniform(MENU_RADIUS);
290 frus_core::paint_elevation(
291 scene,
292 bounds,
293 radius,
294 ELEVATION,
295 theme.scheme.shadow.fade(o),
296 );
297 scene.draw_shape(
298 bounds,
299 ShapeBorder::rounded(radius),
300 theme.scheme.surface.fade(o),
301 );
302 return;
303 }
304 let radius = BorderRadius::uniform(HEIGHT * 0.5);
306 let shape = ShapeBorder::rounded(radius);
307 frus_core::paint_elevation(
309 scene,
310 bounds,
311 radius,
312 ELEVATION,
313 theme.scheme.shadow.fade(o),
314 );
315 scene.draw_shape(bounds, shape, colours(theme).0.fade(o));
316 }
317
318 fn on_click(&self) -> Option<Msg> {
319 None
320 }
321
322 fn opaque(&self) -> bool {
325 true
326 }
327}
328
329struct ToolbarButton<Msg> {
331 label: String,
332 action: Option<EditAction>,
333 message: Option<Msg>,
334 first: bool,
337 last: bool,
338}
339
340impl<Msg> ToolbarButton<Msg> {
341 fn new(item: ToolbarItem<Msg>, first: bool, last: bool) -> Self {
342 let (label, action, message) = match item.kind {
343 ItemKind::Action(action) => (action.label().to_owned(), Some(action), None),
344 ItemKind::Custom { label, message } => (label, None, Some(message)),
345 };
346 Self {
347 label,
348 action,
349 message,
350 first,
351 last,
352 }
353 }
354
355 fn padding(&self) -> (f32, f32) {
357 (
358 if self.first { PAD_END } else { PAD_MIDDLE },
359 if self.last { PAD_END } else { PAD_MIDDLE },
360 )
361 }
362}
363
364fn label_style(theme: Option<&Theme>) -> frus_core::TextStyle {
367 let mut style = crate::theme::type_scale(theme).label_large;
368 style.weight = Some(frus_core::FontWeight::Regular);
369 style
370}
371
372impl<Msg: Clone> Widget<Msg> for ToolbarButton<Msg> {
373 fn style_themed(&self, theme: &Theme) -> Style {
374 let measured = frus_text::measure_style(&self.label, label_style(Some(theme)));
375 let (before, after) = self.padding();
376 Style {
377 width: Dimension::Length((measured.width + before + after).ceil().max(MIN_WIDTH)),
378 height: Dimension::Length(HEIGHT),
379 ..Default::default()
380 }
381 }
382
383 fn style(&self) -> Style {
384 Widget::<Msg>::style_themed(self, &Theme::default())
385 }
386
387 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
388 &[]
389 }
390
391 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
392 let o = status.opacity;
393 let (_, ink) = colours(theme);
394 let layer = theme.state_layer(frus_core::Color::TRANSPARENT, ink, &status);
398 let end = HEIGHT * 0.5;
399 let (start_r, end_r) = (
400 if self.first { end } else { 0.0 },
401 if self.last { end } else { 0.0 },
402 );
403 let radius = BorderRadius {
404 top_left: start_r,
405 bottom_left: start_r,
406 top_right: end_r,
407 bottom_right: end_r,
408 };
409 scene.draw_shape(bounds, ShapeBorder::rounded(radius), layer.fade(o));
410 let resolved = label_style(Some(theme)).resolved();
411 let measured = frus_text::measure_resolved(&self.label, &resolved);
412 let (before, after) = self.padding();
413 let x = bounds.x + before + (bounds.width - before - after - measured.width) / 2.0;
415 scene.text(
416 frus_core::Point::new(x, bounds.y + (bounds.height - measured.height) / 2.0),
417 self.label.clone(),
418 &resolved,
419 ink.fade(o),
420 );
421 }
422
423 fn on_click(&self) -> Option<Msg> {
424 self.message.clone()
425 }
426
427 fn opaque(&self) -> bool {
431 self.action.is_some()
432 }
433
434 fn edit_action(&self) -> Option<EditAction> {
435 self.action
436 }
437
438 fn focusable(&self) -> bool {
441 false
442 }
443
444 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
445 Some(
446 frus_core::SemanticsProperties::new(frus_core::Role::Button)
447 .label(self.label.clone())
448 .clickable(),
449 )
450 }
451}
452
453struct MenuButton<Msg> {
457 label: String,
458 action: Option<EditAction>,
459 message: Option<Msg>,
460}
461
462impl<Msg> MenuButton<Msg> {
463 fn new(item: ToolbarItem<Msg>) -> Self {
464 let (label, action, message) = match item.kind {
465 ItemKind::Action(action) => (action.label().to_owned(), Some(action), None),
466 ItemKind::Custom { label, message } => (label, None, Some(message)),
467 };
468 Self {
469 label,
470 action,
471 message,
472 }
473 }
474}
475
476fn menu_label_style() -> frus_core::TextStyle {
478 frus_core::TextStyle::new(14.0).weight(frus_core::FontWeight::Regular)
479}
480
481fn menu_ink(theme: &Theme) -> frus_core::Color {
483 if theme.scheme.brightness == crate::Brightness::Dark {
484 frus_core::Color::WHITE
485 } else {
486 frus_core::Color::BLACK.fade(0.87)
487 }
488}
489
490impl<Msg: Clone> Widget<Msg> for MenuButton<Msg> {
491 fn style(&self) -> Style {
492 let measured = frus_text::measure_style(&self.label, menu_label_style());
493 Style {
494 width: Dimension::Length(MENU_WIDTH),
495 height: Dimension::Length(ROW_HEIGHT.max(measured.height + ROW_PAD_BOTTOM).ceil()),
496 ..Default::default()
497 }
498 }
499
500 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
501 &[]
502 }
503
504 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
505 let o = status.opacity;
506 let ink = menu_ink(theme);
507 let layer = theme.state_layer(frus_core::Color::TRANSPARENT, ink, &status);
510 scene.draw_rect(
511 bounds,
512 layer.fade(o),
513 0.0,
514 0.0,
515 frus_core::Color::TRANSPARENT,
516 );
517 let resolved = menu_label_style().resolved();
518 let measured = frus_text::measure_resolved(&self.label, &resolved);
519 let room = bounds.height - ROW_PAD_BOTTOM;
521 scene.text(
522 frus_core::Point::new(
523 bounds.x + ROW_PAD_X,
524 bounds.y + (room - measured.height) / 2.0,
525 ),
526 self.label.clone(),
527 &resolved,
528 ink.fade(o),
529 );
530 }
531
532 fn on_click(&self) -> Option<Msg> {
533 self.message.clone()
534 }
535
536 fn opaque(&self) -> bool {
538 self.action.is_some()
539 }
540
541 fn edit_action(&self) -> Option<EditAction> {
542 self.action
543 }
544
545 fn focusable(&self) -> bool {
547 false
548 }
549
550 fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
551 Some(
552 frus_core::SemanticsProperties::new(frus_core::Role::Button)
553 .label(self.label.clone())
554 .clickable(),
555 )
556 }
557}
558
559pub(crate) fn place_menu(anchor: frus_core::Point, size: Size, window: Rect) -> (f32, f32) {
563 let right = window.x + window.width - MENU_SCREEN_PADDING;
564 let bottom = window.y + window.height - MENU_SCREEN_PADDING;
565 let over_x = anchor.x + size.width - right;
566 let over_y = anchor.y + size.height - bottom;
567 (
568 if over_x > 0.0 {
569 anchor.x - over_x
570 } else {
571 anchor.x
572 },
573 if over_y > 0.0 {
574 anchor.y - over_y
575 } else {
576 anchor.y
577 },
578 )
579}
580
581pub(crate) fn primary_anchor(selection: Rect) -> frus_core::Point {
584 frus_core::Point::new(selection.x + selection.width * 0.5, selection.y)
585}
586
587pub(crate) const GAP: f32 = 8.0;
590pub(crate) const MARGIN: f32 = 8.0;
592
593pub(crate) fn place(anchor: Rect, size: Size, window: Rect, below_clear: f32) -> (f32, f32) {
599 let centre = anchor.x + anchor.width * 0.5;
600 let max_x = (window.x + window.width - size.width - MARGIN).max(window.x + MARGIN);
601 let x = (centre - size.width * 0.5).clamp(window.x + MARGIN, max_x);
602 let above = anchor.y - size.height - GAP;
603 let y = if above >= window.y + MARGIN {
604 above
605 } else {
606 let below = anchor.y + anchor.height + below_clear + GAP;
607 below.min((window.y + window.height - size.height - MARGIN).max(window.y))
609 };
610 (x, y)
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 const WINDOW: Rect = Rect::new(0.0, 0.0, 400.0, 800.0);
618
619 #[test]
620 fn every_context_has_its_own_number() {
621 let mut seen = std::collections::HashSet::new();
622 for has_selection in [false, true] {
623 for can_paste in [false, true] {
624 for all_selected in [false, true] {
625 let context = ToolbarContext {
626 has_selection,
627 can_paste,
628 all_selected,
629 };
630 let variant = context.variant();
631 assert!(variant < ToolbarContext::VARIANTS);
632 assert!(seen.insert(variant), "two contexts share {variant}");
633 }
634 }
635 }
636 assert_eq!(seen.len(), usize::from(ToolbarContext::VARIANTS));
637 }
638
639 #[test]
642 fn the_desktops_get_the_menu() {
643 use frus_core::TargetPlatform as P;
644 for platform in P::ALL {
645 let menu = matches!(platform, P::Fuchsia | P::Linux | P::Windows);
646 assert_eq!(uses_menu(platform), menu, "{platform}");
647 }
648 }
649
650 #[test]
653 fn the_menu_hangs_from_its_anchor_inside_the_window() {
654 let size = Size::new(MENU_WIDTH, 144.0);
655 let at = |x: f32, y: f32| place_menu(frus_core::Point::new(x, y), size, WINDOW);
656 assert_eq!(at(50.0, 60.0), (50.0, 60.0), "room: at the anchor");
657 assert_eq!(
658 at(300.0, 60.0).0,
659 400.0 - 8.0 - MENU_WIDTH,
660 "pushed back from the right"
661 );
662 assert_eq!(
663 at(50.0, 750.0).1,
664 800.0 - 8.0 - 144.0,
665 "and up from the bottom"
666 );
667 assert_eq!(
668 primary_anchor(Rect::new(100.0, 40.0, 60.0, 20.0)),
669 frus_core::Point::new(130.0, 40.0),
670 "a selection's anchor is the middle of its top"
671 );
672 }
673
674 #[test]
678 fn on_the_desktops_it_is_a_menu_of_rows() {
679 let theme = Theme::dark().with_platform(frus_core::TargetPlatform::Linux);
680 let ui = crate::build_ui(
681 &four(),
682 Size::new(600.0, 400.0),
683 &crate::Runtime::default(),
684 &theme,
685 );
686 let words: Vec<(String, frus_core::Point)> = ui
687 .scene()
688 .primitives()
689 .iter()
690 .filter_map(|p| match p {
691 frus_core::Primitive::Text { text, position, .. } => {
692 Some((text.clone(), *position))
693 }
694 _ => None,
695 })
696 .collect();
697 let labels: Vec<&str> = words.iter().map(|(t, _)| t.as_str()).collect();
698 assert_eq!(labels, ["Cut", "Copy", "Paste", "Select all"]);
699 for (i, (_, origin)) in words.iter().enumerate() {
700 assert_eq!(origin.x, ROW_PAD_X, "at the start");
701 assert!(
702 origin.y >= ROW_HEIGHT * i as f32 && origin.y < ROW_HEIGHT * (i + 1) as f32,
703 "row {i} of 36 px: {origin:?}"
704 );
705 }
706 let panel = ui
707 .scene()
708 .primitives()
709 .iter()
710 .find_map(|p| match p {
711 frus_core::Primitive::Rect {
712 rect,
713 color,
714 radius,
715 ..
716 } if rect.width == MENU_WIDTH && *color == theme.scheme.surface => {
717 Some((*rect, radius.top_left))
718 }
719 _ => None,
720 })
721 .expect("the menu's surface");
722 assert_eq!(panel.0.height, ROW_HEIGHT * 4.0);
723 assert_eq!(panel.1, MENU_RADIUS);
724 }
725
726 #[test]
727 fn the_bar_sits_above_the_selection_centred_on_it() {
728 let anchor = Rect::new(150.0, 300.0, 100.0, 20.0);
729 let size = Size::new(200.0, 44.0);
730 let (x, y) = place(anchor, size, WINDOW, 22.0);
731 assert_eq!(x, 100.0, "centred: 200 + 0 - 100");
732 assert_eq!(y, 300.0 - 44.0 - GAP);
733 }
734
735 #[test]
736 fn with_no_room_above_it_goes_below_and_clears_the_handles() {
737 let anchor = Rect::new(150.0, 20.0, 100.0, 20.0);
738 let size = Size::new(200.0, 44.0);
739 let (_, y) = place(anchor, size, WINDOW, 22.0);
740 assert_eq!(y, 20.0 + 20.0 + 22.0 + GAP);
741 }
742
743 #[test]
744 fn against_an_edge_it_is_held_inside_the_window() {
745 let size = Size::new(200.0, 44.0);
746 let left = place(Rect::new(0.0, 300.0, 30.0, 20.0), size, WINDOW, 22.0);
748 assert_eq!(left.0, MARGIN);
749 let right = place(Rect::new(380.0, 300.0, 20.0, 20.0), size, WINDOW, 22.0);
751 assert_eq!(right.0, 400.0 - 200.0 - MARGIN);
752 }
753
754 #[test]
755 fn a_window_too_narrow_for_the_bar_pins_it_to_the_left_margin() {
756 let (x, _) = place(
757 Rect::new(10.0, 300.0, 20.0, 20.0),
758 Size::new(500.0, 44.0),
759 WINDOW,
760 22.0,
761 );
762 assert_eq!(x, MARGIN);
763 }
764
765 #[test]
766 fn below_the_selection_it_still_stays_on_the_window() {
767 let window = Rect::new(0.0, 0.0, 400.0, 100.0);
769 let (_, y) = place(
770 Rect::new(150.0, 30.0, 100.0, 20.0),
771 Size::new(200.0, 44.0),
772 window,
773 22.0,
774 );
775 assert_eq!(y, 100.0 - 44.0 - MARGIN);
776 }
777
778 #[test]
779 fn a_built_in_item_says_its_action_and_an_application_item_does_not() {
780 assert_eq!(
781 ToolbarItem::<()>::cut().edit_action(),
782 Some(EditAction::Cut)
783 );
784 assert_eq!(ToolbarItem::<()>::select_all().label(), "Select all");
785 let own = ToolbarItem::custom("Translate", 7);
786 assert_eq!(own.edit_action(), None);
787 assert_eq!(own.label(), "Translate");
788 }
789
790 #[test]
791 fn a_built_in_button_is_a_target_without_a_message_and_never_takes_the_focus() {
792 let button = ToolbarButton::<u8>::new(ToolbarItem::copy(), true, true);
793 assert!(Widget::<u8>::opaque(&button));
794 assert_eq!(Widget::<u8>::on_click(&button), None);
795 assert_eq!(Widget::<u8>::edit_action(&button), Some(EditAction::Copy));
796 assert!(!Widget::<u8>::focusable(&button));
797 }
798
799 #[test]
800 fn an_application_button_sends_its_message_and_is_not_an_edit() {
801 let button = ToolbarButton::new(ToolbarItem::custom("Translate", 7u8), true, true);
802 assert_eq!(Widget::<u8>::on_click(&button), Some(7));
803 assert_eq!(Widget::<u8>::edit_action(&button), None);
804 assert!(!Widget::<u8>::opaque(&button));
805 }
806
807 fn four() -> SelectionToolbar<u8> {
809 SelectionToolbar::new(vec![
810 ToolbarItem::cut(),
811 ToolbarItem::copy(),
812 ToolbarItem::paste(),
813 ToolbarItem::select_all(),
814 ])
815 }
816
817 fn frame(theme: &Theme) -> crate::Ui<u8> {
820 crate::build_ui(
821 &four(),
822 Size::new(600.0, 100.0),
823 &crate::Runtime::default(),
824 &theme
825 .clone()
826 .with_platform(frus_core::TargetPlatform::Android),
827 )
828 }
829
830 #[test]
834 fn it_is_a_pill_just_off_the_page_in_the_platforms_colours() {
835 for (theme, surface, ink) in [
836 (
837 Theme::light(),
838 frus_core::Color::WHITE,
839 frus_core::Color::BLACK,
840 ),
841 (
842 Theme::dark(),
843 frus_core::Color::rgb(
844 0x42 as f32 / 255.0,
845 0x42 as f32 / 255.0,
846 0x42 as f32 / 255.0,
847 ),
848 frus_core::Color::WHITE,
849 ),
850 ] {
851 let ui = frame(&theme);
852 let bar = ui
853 .scene()
854 .primitives()
855 .iter()
856 .find_map(|p| match p {
857 frus_core::Primitive::Rect {
858 rect,
859 color,
860 radius,
861 blur,
862 ..
863 } if *blur == 0.0 && *color == surface => Some((*rect, *radius)),
864 _ => None,
865 })
866 .expect("the bar's surface");
867 assert_eq!(bar.0.height, HEIGHT);
868 assert_eq!(bar.1, BorderRadius::uniform(HEIGHT * 0.5), "round ends");
869 assert_eq!(crate::shadowprobe::height(ui.scene().primitives()), 1.0);
870 let words: Vec<_> = ui
871 .scene()
872 .primitives()
873 .iter()
874 .filter_map(|p| match p {
875 frus_core::Primitive::Text { color, .. } => Some(*color),
876 _ => None,
877 })
878 .collect();
879 assert_eq!(words, vec![ink; 4]);
880 }
881 }
882
883 #[test]
885 fn a_theme_of_its_own_colours_it() {
886 let mut theme = Theme::light();
887 theme.scheme.surface = frus_core::Color::rgb(0.9, 0.95, 1.0);
888 theme.scheme.on_surface = frus_core::Color::rgb(0.1, 0.1, 0.3);
889 assert_eq!(
890 colours(&theme),
891 (theme.scheme.surface, theme.scheme.on_surface)
892 );
893 }
894
895 #[test]
899 fn the_ends_keep_more_room_and_a_short_word_a_wide_enough_target() {
900 let theme = Theme::light();
901 let style = label_style(Some(&theme));
902 let width = |label: &str| frus_text::measure_style(label, style).width;
903 let first = ToolbarButton::<u8>::new(ToolbarItem::cut(), true, false);
904 let middle = ToolbarButton::<u8>::new(ToolbarItem::copy(), false, false);
905 let last = ToolbarButton::<u8>::new(ToolbarItem::select_all(), false, true);
906 let styled = |b: &ToolbarButton<u8>| match Widget::<u8>::style_themed(b, &theme).width {
907 Dimension::Length(w) => w,
908 _ => unreachable!(),
909 };
910 assert_eq!(
911 styled(&first),
912 (width("Cut") + PAD_END + PAD_MIDDLE).ceil().max(MIN_WIDTH)
913 );
914 assert_eq!(
915 styled(&middle),
916 (width("Copy") + 2.0 * PAD_MIDDLE).ceil().max(MIN_WIDTH)
917 );
918 assert_eq!(
919 styled(&last),
920 (width("Select all") + PAD_MIDDLE + PAD_END).ceil()
921 );
922 let tiny = ToolbarButton::<u8>::new(ToolbarItem::custom("A", 1), false, false);
923 assert_eq!(styled(&tiny), MIN_WIDTH);
924 }
925}