use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::platform::primary_modifier_label;
use crate::text::Font;
use crate::widget::Widget;
use crate::widgets::flex::FlexColumn;
use super::commands::CommonStyle;
use super::editor::RichEditHandle;
use super::model::ListKind;
mod color;
mod controls;
use crate::widgets::text_area::TextHAlign;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum PickerKind {
None,
TextColor,
Highlight,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Variant {
Bold,
Italic,
}
pub(crate) type VariantCheck = Rc<dyn Fn(&str, Variant) -> bool>;
struct Families {
names: Vec<String>,
item_fonts: Option<Vec<Arc<Font>>>,
}
#[derive(Clone, Copy)]
struct ToolbarConfig {
bold: bool,
italic: bool,
underline: bool,
strikethrough: bool,
alignment: bool,
lists: bool,
indent: bool,
history: bool,
font_size: bool,
text_color: bool,
highlight: bool,
tooltips: bool,
}
impl Default for ToolbarConfig {
fn default() -> Self {
Self {
bold: true,
italic: true,
underline: true,
strikethrough: true,
alignment: true,
lists: true,
indent: true,
history: true,
font_size: true,
text_color: true,
highlight: true,
tooltips: true,
}
}
}
const DEFAULT_FONT_SIZES: &[f64] = &[
8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 32.0,
];
const OVERLAY_LAYOUT_ROOM: Size = Size::new(4096.0, 4096.0);
pub struct RichTextToolbar {
bounds: Rect,
base: WidgetBase,
root: Vec<Box<dyn Widget>>,
handle: RichEditHandle,
font: Arc<Font>,
cfg: ToolbarConfig,
families: Option<Families>,
variant_check: Option<VariantCheck>,
font_sizes: Vec<f64>,
picker: Rc<Cell<PickerKind>>,
}
impl RichTextToolbar {
pub fn new(handle: RichEditHandle, font: Arc<Font>) -> Self {
let mut this = Self {
bounds: Rect::default(),
base: WidgetBase::new(),
root: Vec::new(),
handle,
font,
cfg: ToolbarConfig::default(),
families: None,
variant_check: None,
font_sizes: DEFAULT_FONT_SIZES.to_vec(),
picker: Rc::new(Cell::new(PickerKind::None)),
};
this.rebuild();
this
}
pub fn with_bold(mut self, on: bool) -> Self {
self.cfg.bold = on;
self.rebuild();
self
}
pub fn with_italic(mut self, on: bool) -> Self {
self.cfg.italic = on;
self.rebuild();
self
}
pub fn with_underline(mut self, on: bool) -> Self {
self.cfg.underline = on;
self.rebuild();
self
}
pub fn with_strikethrough(mut self, on: bool) -> Self {
self.cfg.strikethrough = on;
self.rebuild();
self
}
pub fn with_alignment(mut self, on: bool) -> Self {
self.cfg.alignment = on;
self.rebuild();
self
}
pub fn with_lists(mut self, on: bool) -> Self {
self.cfg.lists = on;
self.rebuild();
self
}
pub fn with_indent(mut self, on: bool) -> Self {
self.cfg.indent = on;
self.rebuild();
self
}
pub fn with_history(mut self, on: bool) -> Self {
self.cfg.history = on;
self.rebuild();
self
}
pub fn with_font_size_combo(mut self, on: bool) -> Self {
self.cfg.font_size = on;
self.rebuild();
self
}
pub fn with_colors(mut self, on: bool) -> Self {
self.cfg.text_color = on;
self.cfg.highlight = on;
self.rebuild();
self
}
pub fn with_tooltips(mut self, on: bool) -> Self {
self.cfg.tooltips = on;
self.rebuild();
self
}
pub fn with_font_sizes(mut self, sizes: Vec<f64>) -> Self {
self.font_sizes = sizes;
self.rebuild();
self
}
pub fn with_families(mut self, names: Vec<String>, item_fonts: Option<Vec<Arc<Font>>>) -> Self {
self.families = if names.is_empty() {
None
} else {
Some(Families { names, item_fonts })
};
self.rebuild();
self
}
pub fn with_variant_check(mut self, check: impl Fn(&str, Variant) -> bool + 'static) -> Self {
self.variant_check = Some(Rc::new(check));
self.rebuild();
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
fn rebuild(&mut self) {
let mut col = FlexColumn::new().with_gap(6.0);
let row1 = self.build_row1();
if !row1.children().is_empty() {
col.push(row1, 0.0);
}
let row2 = self.build_row2();
if !row2.children().is_empty() {
col.push(row2, 0.0);
}
let mut root: Vec<Box<dyn Widget>> = vec![Box::new(col)];
if self.cfg.text_color || self.cfg.highlight {
root.push(color::color_overlay(&self.font, &self.handle, &self.picker));
}
self.root = root;
}
fn tip(&self, mut w: Box<dyn Widget>, text: impl Into<String>) -> Box<dyn Widget> {
if self.cfg.tooltips {
w.set_tooltip_text(Some(text.into()));
}
w
}
fn build_row1(&self) -> Box<dyn Widget> {
let mut row = controls::new_row();
let check = self.variant_check.as_ref();
if self.cfg.bold {
row = row.add(self.tip(
controls::style_toggle(
&self.font,
&self.handle,
controls::ICON_BOLD,
|c| c.bold,
super::commands::RichCommand::ToggleBold,
Some(Variant::Bold),
check,
),
"Bold",
));
}
if self.cfg.italic {
row = row.add(self.tip(
controls::style_toggle(
&self.font,
&self.handle,
controls::ICON_ITALIC,
|c| c.italic,
super::commands::RichCommand::ToggleItalic,
Some(Variant::Italic),
check,
),
"Italic",
));
}
if self.cfg.underline {
row = row.add(self.tip(
controls::style_toggle(
&self.font,
&self.handle,
controls::ICON_UNDERLINE,
|c| c.underline,
super::commands::RichCommand::ToggleUnderline,
None,
None,
),
"Underline",
));
}
if self.cfg.strikethrough {
row = row.add(self.tip(
controls::style_toggle(
&self.font,
&self.handle,
controls::ICON_STRIKE,
|c| c.strikethrough,
super::commands::RichCommand::ToggleStrikethrough,
None,
None,
),
"Strikethrough",
));
}
if let Some(families) = &self.families {
row = row.add(self.tip(
controls::family_combo(&self.font, &self.handle, families),
"Font family",
));
}
if self.cfg.font_size {
row = row.add(self.tip(
controls::size_combo(&self.font, &self.handle, &self.font_sizes),
"Font size",
));
}
if self.cfg.text_color {
row = row.add(self.tip(
color::text_color_button(&self.font, &self.picker),
"Text color",
));
}
if self.cfg.highlight {
row = row.add(self.tip(
color::highlight_button(&self.font, &self.picker),
"Highlight color",
));
row = row.add(self.tip(
color::remove_highlight_button(&self.font, &self.handle),
"Remove highlight",
));
}
Box::new(row)
}
fn build_row2(&self) -> Box<dyn Widget> {
let modifier = primary_modifier_label();
let mut row = controls::new_row();
if self.cfg.alignment {
row = row.add(self.tip(
controls::align_toggle(&self.font, &self.handle, controls::ICON_ALIGN_LEFT, TextHAlign::Left),
"Align left",
));
row = row.add(self.tip(
controls::align_toggle(&self.font, &self.handle, controls::ICON_ALIGN_CENTER, TextHAlign::Center),
"Align center",
));
row = row.add(self.tip(
controls::align_toggle(&self.font, &self.handle, controls::ICON_ALIGN_RIGHT, TextHAlign::Right),
"Align right",
));
}
if self.cfg.lists {
row = row.add(self.tip(
controls::list_toggle(&self.font, &self.handle, controls::ICON_LIST_OL, ListKind::Ordered),
"Numbered list",
));
row = row.add(self.tip(
controls::list_toggle(&self.font, &self.handle, controls::ICON_LIST_UL, ListKind::Bullet),
"Bulleted list",
));
}
if self.cfg.indent {
row = row.add(self.tip(
controls::command_button(&self.font, &self.handle, controls::ICON_OUTDENT, super::commands::RichCommand::Outdent),
"Decrease indent",
));
row = row.add(self.tip(
controls::command_button(&self.font, &self.handle, controls::ICON_INDENT, super::commands::RichCommand::Indent),
"Increase indent",
));
}
if self.cfg.history {
row = row.add(self.tip(
controls::undo_button(&self.font, &self.handle),
format!("Undo ({modifier}+Z)"),
));
row = row.add(self.tip(
controls::redo_button(&self.font, &self.handle),
format!("Redo ({modifier}+Y)"),
));
}
Box::new(row)
}
pub fn common_style_of_selection(&self) -> CommonStyle {
self.handle.common_style_of_selection()
}
}
impl Widget for RichTextToolbar {
fn type_name(&self) -> &'static str {
"RichTextToolbar"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.root
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.root
}
fn margin(&self) -> Insets {
self.base.margin
}
fn widget_base(&self) -> Option<&WidgetBase> {
Some(&self.base)
}
fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
Some(&mut self.base)
}
fn h_anchor(&self) -> HAnchor {
self.base.h_anchor
}
fn v_anchor(&self) -> VAnchor {
self.base.v_anchor
}
fn min_size(&self) -> Size {
self.base.min_size
}
fn max_size(&self) -> Size {
self.base.max_size
}
fn measure_min_height(&self, available_w: f64) -> f64 {
self.root
.first()
.map(|c| c.measure_min_height(available_w))
.unwrap_or(0.0)
}
fn layout(&mut self, available: Size) -> Size {
let mut reported = Size::new(0.0, 0.0);
for (i, child) in self.root.iter_mut().enumerate() {
if i == 0 {
let desired = child.layout(available);
child.set_bounds(Rect::new(0.0, 0.0, desired.width, desired.height));
reported = desired;
} else {
child.layout(OVERLAY_LAYOUT_ROOM);
child.set_bounds(Rect::new(0.0, 0.0, 0.0, 0.0));
}
}
self.bounds = Rect::new(0.0, 0.0, reported.width, reported.height);
reported
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
}
impl Drop for RichTextToolbar {
fn drop(&mut self) {
if self.picker.get() != PickerKind::None {
self.handle.cancel_preview();
self.picker.set(PickerKind::None);
}
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;