use std::sync::Arc;
use crate::color::Color;
use crate::event::{Event, Modifiers, MouseButton};
use crate::geometry::{Point, Size};
use crate::text::Font;
use crate::widget::Widget;
use crate::widgets::rich_text::commands::RichCommand;
use crate::widgets::rich_text::model::{Block, InlineStyle, RichDoc, TextRun};
use crate::widgets::rich_text::view::SharedResolver;
use crate::widgets::rich_text::{RichEditHandle, RichTextEdit};
use super::RichTextToolbar;
const ICON_ERASER: &str = "\u{F12D}";
const FONT_BYTES: &[u8] = include_bytes!("../../../../../demo/assets/CascadiaCode.ttf");
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(FONT_BYTES).expect("test font loads"))
}
fn resolver(font: &Arc<Font>) -> SharedResolver {
let f = Arc::clone(font);
std::rc::Rc::new(move |_: &InlineStyle| Arc::clone(&f))
}
fn toolbar_over(doc: RichDoc) -> (RichTextToolbar, RichEditHandle) {
let font = font();
let editor = RichTextEdit::new(doc, resolver(&font));
let handle = editor.handle();
(RichTextToolbar::new(handle.clone(), font), handle)
}
fn child_type_names(w: &dyn Widget) -> Vec<&'static str> {
w.children().iter().map(|c| c.type_name()).collect()
}
fn tip_text(w: &dyn Widget) -> Option<String> {
w.tooltip_text().map(str::to_string)
}
fn rows(toolbar: &RichTextToolbar) -> Vec<&dyn Widget> {
let col = toolbar.children()[0].as_ref();
col.children().iter().map(|c| c.as_ref()).collect()
}
#[test]
fn full_roster_with_families() {
let (toolbar, _h) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
let toolbar = toolbar.with_families(vec!["Sans".into(), "Serif".into()], None);
let rows = rows(&toolbar);
assert_eq!(rows.len(), 2, "two rows");
assert_eq!(
child_type_names(rows[0]),
["Button", "Button", "Button", "Button", "ComboBox", "ComboBox", "Button", "Button", "Button"],
"row 1: B/I/U/S, family combo, size combo, text-colour, highlight, remove-highlight"
);
assert_eq!(child_type_names(rows[1]), vec!["Button"; 9], "row 2 roster");
}
#[test]
fn config_disables_controls_and_empty_rows() {
let (toolbar, _h) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
let toolbar = toolbar
.with_bold(false)
.with_colors(false)
.with_history(false)
.with_alignment(false)
.with_lists(false)
.with_indent(false);
let rows = rows(&toolbar);
assert_eq!(rows.len(), 1, "empty second row omitted");
assert_eq!(
child_type_names(rows[0]),
["Button", "Button", "Button", "ComboBox"],
"row 1 lost bold + colours, kept I/U/S + size combo"
);
}
#[test]
fn family_omitted_when_no_families() {
let (toolbar, _h) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
let rows = rows(&toolbar);
assert_eq!(
child_type_names(rows[0]),
["Button", "Button", "Button", "Button", "ComboBox", "Button", "Button", "Button"],
"row 1 without a family combo: B/I/U/S, size combo, two colour swatches, remove-highlight"
);
let combo_count = child_type_names(rows[0]).iter().filter(|t| **t == "ComboBox").count();
assert_eq!(combo_count, 1, "only the size ComboBox, no family ComboBox");
}
#[test]
fn default_controls_carry_expected_tooltips() {
let (toolbar, _h) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
let toolbar = toolbar.with_families(vec!["Sans".into(), "Serif".into()], None);
let rows = rows(&toolbar);
let row1: Vec<Option<String>> = rows[0].children().iter().map(|c| tip_text(c.as_ref())).collect();
assert_eq!(row1[0].as_deref(), Some("Bold"));
assert_eq!(row1[1].as_deref(), Some("Italic"));
assert_eq!(row1[2].as_deref(), Some("Underline"));
assert_eq!(row1[3].as_deref(), Some("Strikethrough"));
assert_eq!(row1[4].as_deref(), Some("Font family"));
assert_eq!(row1[5].as_deref(), Some("Font size"));
assert_eq!(row1[6].as_deref(), Some("Text color"));
assert_eq!(row1[7].as_deref(), Some("Highlight color"));
assert_eq!(row1[8].as_deref(), Some("Remove highlight"));
let row2: Vec<Option<String>> = rows[1].children().iter().map(|c| tip_text(c.as_ref())).collect();
assert_eq!(row2[0].as_deref(), Some("Align left"));
assert_eq!(row2[1].as_deref(), Some("Align center"));
assert_eq!(row2[2].as_deref(), Some("Align right"));
assert_eq!(row2[3].as_deref(), Some("Numbered list"));
assert_eq!(row2[4].as_deref(), Some("Bulleted list"));
assert_eq!(row2[5].as_deref(), Some("Decrease indent"));
assert_eq!(row2[6].as_deref(), Some("Increase indent"));
let m = crate::platform::primary_modifier_label();
assert_eq!(row2[7].as_deref(), Some(format!("Undo ({m}+Z)").as_str()));
assert_eq!(row2[8].as_deref(), Some(format!("Redo ({m}+Y)").as_str()));
}
#[test]
fn with_tooltips_false_wraps_nothing() {
let (toolbar, _h) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
let toolbar = toolbar
.with_families(vec!["Sans".into(), "Serif".into()], None)
.with_tooltips(false);
let mut names = Vec::new();
descendant_type_names(toolbar.children()[0].as_ref(), &mut names);
assert!(
!names.contains(&"Tooltip"),
"with_tooltips(false) must not wrap any control; tree was {names:?}"
);
assert!(
!any_descendant_has_tip(toolbar.children()[0].as_ref()),
"with_tooltips(false) must not set a tip on any control"
);
}
fn any_descendant_has_tip(w: &dyn Widget) -> bool {
w.tooltip_text().is_some() || w.children().iter().any(|c| any_descendant_has_tip(c.as_ref()))
}
fn bold_button(toolbar: &mut RichTextToolbar) -> &mut Box<dyn Widget> {
let col = &mut toolbar.children_mut()[0];
let row1 = &mut col.children_mut()[0];
&mut row1.children_mut()[0]
}
#[test]
fn click_bold_bolds_selection_through_handle() {
let (mut toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hello")]));
handle.select_all();
assert_ne!(handle.common_style_of_selection().bold, Some(true), "not bold yet");
toolbar.layout(Size::new(600.0, 100.0));
let button = bold_button(&mut toolbar);
let b = button.bounds();
let pos = Point::new(b.width * 0.5, b.height * 0.5);
let down = Event::MouseDown { pos, button: MouseButton::Left, modifiers: Modifiers::default() };
let up = Event::MouseUp { pos, button: MouseButton::Left, modifiers: Modifiers::default() };
button.on_event(&down);
button.on_event(&up);
assert_eq!(
handle.common_style_of_selection().bold,
Some(true),
"clicking Bold bolded the whole selection through the handle"
);
assert_eq!(handle.plain_text(), "hello", "text unchanged by formatting");
}
#[test]
fn bold_toggle_reflects_tri_state() {
let bold = InlineStyle { bold: true, ..Default::default() };
let (toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::from_run(
TextRun::new("bold", bold.clone()),
)]));
handle.select_all();
let cs = toolbar.common_style_of_selection();
assert_eq!(cs.bold, Some(true));
assert!(cs.bold == Some(true), "uniform bold ⇒ toggle active");
let (toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block {
runs: vec![TextRun::new("B", bold), TextRun::plain("p")],
..Block::new()
}]));
handle.select_all();
let cs = toolbar.common_style_of_selection();
assert_eq!(cs.bold, None, "mixed selection reports None");
assert!(cs.bold != Some(true), "mixed ⇒ toggle inactive");
let (toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("plain")]));
handle.select_all();
assert_eq!(toolbar.common_style_of_selection().bold, Some(false));
}
fn descendant_type_names(w: &dyn Widget, out: &mut Vec<&'static str>) {
out.push(w.type_name());
for c in w.children() {
descendant_type_names(c.as_ref(), out);
}
}
fn open_picker_type_names(row1_index: usize) -> Vec<&'static str> {
let (mut toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
handle.select_all();
toolbar.layout(Size::new(600.0, 100.0));
{
let col = &mut toolbar.children_mut()[0];
let row1 = &mut col.children_mut()[0];
let swatch = &mut row1.children_mut()[row1_index];
let b = swatch.bounds();
let pos = Point::new(b.width * 0.5, b.height * 0.5);
swatch.on_event(&Event::MouseDown { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
swatch.on_event(&Event::MouseUp { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
}
toolbar.layout(Size::new(600.0, 100.0));
assert!(handle.is_previewing(), "opening the swatch begins a preview session");
let mut names = Vec::new();
descendant_type_names(toolbar.children()[1].as_ref(), &mut names);
names
}
#[test]
fn highlight_picker_has_no_no_color_checkbox() {
let names = open_picker_type_names(6);
assert!(
!names.contains(&"Checkbox"),
"Highlight picker must not build a \"No Color (Pass Through)\" checkbox; \
tree was {names:?}"
);
}
#[test]
fn text_color_picker_has_no_no_color_checkbox() {
let names = open_picker_type_names(5);
assert!(
!names.contains(&"Checkbox"),
"Text-colour picker must not build a \"No Color (Pass Through)\" checkbox; \
tree was {names:?}"
);
}
fn descendant_renders_glyph(w: &dyn Widget, glyph: &str) -> bool {
if w.properties().iter().any(|(k, v)| *k == "text" && v == glyph) {
return true;
}
w.children().iter().any(|c| descendant_renders_glyph(c.as_ref(), glyph))
}
#[test]
fn remove_highlight_button_clears_selection_highlight() {
let (mut toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
handle.select_all();
let yellow = Color::from_rgb8(255, 240, 60);
handle.exec(&RichCommand::SetHighlight(Some(yellow)));
assert_eq!(
handle.common_style_of_selection().highlight,
Some(Some(yellow)),
"selection is uniformly highlighted before the click"
);
toolbar.layout(Size::new(600.0, 100.0));
assert!(
descendant_renders_glyph(toolbar.children()[0].as_ref(), ICON_ERASER),
"toolbar row must contain the Remove-highlight (eraser) button"
);
let button = {
let col = &mut toolbar.children_mut()[0];
let row1 = &mut col.children_mut()[0];
&mut row1.children_mut()[7]
};
let b = button.bounds();
let pos = Point::new(b.width * 0.5, b.height * 0.5);
button.on_event(&Event::MouseDown { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
button.on_event(&Event::MouseUp { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
assert_eq!(
handle.common_style_of_selection().highlight,
Some(None),
"clicking Remove highlight cleared the selection's highlight to None"
);
}
#[test]
fn drop_mid_preview_cancels_session() {
let (mut toolbar, handle) = toolbar_over(RichDoc::from_blocks(vec![Block::plain("hi")]));
handle.select_all();
toolbar.layout(Size::new(600.0, 100.0));
{
let col = &mut toolbar.children_mut()[0];
let row1 = &mut col.children_mut()[0];
let swatch = &mut row1.children_mut()[5];
let b = swatch.bounds();
let pos = Point::new(b.width * 0.5, b.height * 0.5);
swatch.on_event(&Event::MouseDown { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
swatch.on_event(&Event::MouseUp { pos, button: MouseButton::Left, modifiers: Modifiers::default() });
}
toolbar.layout(Size::new(600.0, 100.0));
assert!(handle.is_previewing(), "opening the swatch begins a preview session");
drop(toolbar);
assert!(
!handle.is_previewing(),
"dropping the toolbar mid-preview must cancel the session"
);
}