Skip to main content

odox_ui/
toolbar.rs

1//! The row of buttons over a page being edited. DESIGN.md ยง11.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5
6use eframe::egui::{Key, KeyboardShortcut, Modifiers, RichText, Ui};
7use egui_richedit::{Mark, Model};
8
9use crate::i18n::t;
10use crate::{Block, FlowModel, PageEditor};
11
12/// Bold, italic, underline and strikethrough, each lit where the whole
13/// selection has it, and none of them live without a caret. Answers the mark
14/// whose button was pressed, for the editor to toggle over the model.
15pub fn marks<M: Model<Paragraph = Vec<usize>>>(
16    ui: &mut Ui,
17    editor: &PageEditor,
18    model: &M,
19) -> Option<Mark> {
20    let mut pressed = None;
21    ui.add_enabled_ui(editor.selection().is_some(), |ui| {
22        ui.horizontal(|ui| pressed = mark_buttons(ui, editor, model));
23    });
24    pressed
25}
26
27/// What was pressed on a text document's toolbar.
28#[derive(Default)]
29pub struct Pressed {
30    /// A mark's button, for the editor to toggle over the model.
31    pub mark: Option<Mark>,
32    /// A kind of paragraph's button, for the model to apply.
33    pub block: Option<Block>,
34}
35
36/// The marks, and after them the kinds of paragraph: body text, three
37/// levels of heading, a bulleted and a numbered list. Each is lit where every
38/// paragraph the selection runs over is one, and pressing a lit one takes it
39/// off.
40pub fn text(ui: &mut Ui, editor: &PageEditor, model: &FlowModel<'_>) -> Pressed {
41    let mut pressed = Pressed::default();
42    ui.add_enabled_ui(editor.selection().is_some(), |ui| {
43        ui.horizontal(|ui| {
44            pressed.mark = mark_buttons(ui, editor, model);
45            ui.separator();
46            for (block, label, name) in [
47                (Block::Body, "\u{b6}", t("Body text")),
48                (Block::Heading(1), "H1", t("Heading 1")),
49                (Block::Heading(2), "H2", t("Heading 2")),
50                (Block::Heading(3), "H3", t("Heading 3")),
51                (Block::Bullets, "\u{2022}", t("Bulleted list")),
52                (Block::Numbers, "1.", t("Numbered list")),
53            ] {
54                let lit = editor
55                    .selection()
56                    .is_some_and(|selection| model.is_block(selection, block));
57                if ui
58                    .selectable_label(lit, label)
59                    .on_hover_text(name)
60                    .clicked()
61                {
62                    pressed.block = Some(block);
63                }
64            }
65        });
66    });
67    pressed
68}
69
70fn mark_buttons<M: Model<Paragraph = Vec<usize>>>(
71    ui: &mut Ui,
72    editor: &PageEditor,
73    model: &M,
74) -> Option<Mark> {
75    let mut pressed = None;
76    for mark in Mark::ALL {
77        let lit = editor.marked(model, mark) == Some(true);
78        let (label, name, key) = match mark {
79            Mark::Bold => (RichText::new(t("B")).strong(), t("Bold"), Some(Key::B)),
80            Mark::Italic => (RichText::new(t("I")).italics(), t("Italic"), Some(Key::I)),
81            Mark::Underline => (
82                RichText::new(t("U")).underline(),
83                t("Underline"),
84                Some(Key::U),
85            ),
86            Mark::Strike => (
87                RichText::new(t("S")).strikethrough(),
88                t("Strikethrough"),
89                None,
90            ),
91        };
92        let hint = match key {
93            Some(key) => format!(
94                "{name} ({})",
95                ui.ctx()
96                    .format_shortcut(&KeyboardShortcut::new(Modifiers::COMMAND, key))
97            ),
98            None => name.to_owned(),
99        };
100        if ui
101            .selectable_label(lit, label)
102            .on_hover_text(hint)
103            .clicked()
104        {
105            pressed = Some(mark);
106        }
107    }
108    pressed
109}