//! Ribbon-style widgets: icon buttons, large buttons, split buttons, colour palettes, and the //! message box / toast overlays. use egui::{Align2, Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use crate::SheetApp; pub use crate::code_editor::{Lang, code_editor}; use crate::i18n; use crate::icons::{self, Icon}; use crate::theme::{self, Cat, Tokens}; /// Translates a label or tooltip for the language of the frame being drawn (English if untranslated). /// Ribbon chrome all flows through these widgets, so translating here localises it in one place. fn tr<'a>(ui: &Ui, text: &'a str) -> &'a str { i18n::current(ui.ctx()).tr(text) } /// Keyboard shortcuts shown after a ribbon tooltip's command name: Excel for Mac's on macOS, /// Excel for Windows' elsewhere (each one GridCraft binds). Only the name is translated. const TOOLTIP_SHORTCUTS: &[(&str, &str, &str)] = &[ ("Save", "⌘S", "Ctrl+S"), ("Undo", "⌘Z", "Ctrl+Z"), ("Redo", "⌘Y", "Ctrl+Y"), ("Paste", "⌘V", "Ctrl+V"), ("Cut", "⌘X", "Ctrl+X"), ("Copy", "⌘C", "Ctrl+C"), ("Bold", "⌘B", "Ctrl+B"), ("Italic", "⌘I", "Ctrl+I"), ("Underline", "⌘U", "Ctrl+U"), ("AutoSum", "⌘⇧T", "Alt+="), ("Table", "⌘T", "Ctrl+T"), ("Insert Link", "⌘K", "Ctrl+K"), ("Filter", "⌘⇧F", "Ctrl+Shift+L"), ("Flash Fill", "⌘E", "Ctrl+E"), // The Control key, written the way each platform names it. macOS's ⌃ is supplied by its // system fonts; elsewhere the Windows spelling avoids a glyph no bundled face has. ("Show Formulas", "⌃`", "Ctrl+`"), ]; /// The shortcut a ribbon tooltip shows after `tip`, for this platform (`mac`). pub fn tooltip_shortcut(tip: &str, mac: bool) -> Option<&'static str> { TOOLTIP_SHORTCUTS.iter().find(|(name, _, _)| *name == tip).map(|(_, m, other)| if mac { *m } else { *other }) } /// A ribbon tooltip: the translated command name, then its shortcut for this platform, as Excel /// shows them ("Bold (Ctrl+B)" on Windows, "Bold (⌘B)" on macOS). fn tip_text(ui: &Ui, tip: &str) -> String { // A name shared with a two-line ribbon label (Czech "Automatický\nsoučet") reads as one line. let name = tr(ui, tip).replace('\n', " "); let text = match tooltip_shortcut(tip, cfg!(target_os = "macos")) { Some(keys) => format!("{name} ({keys})"), None => name, }; // A tooltip must not paint a tofu box on a host whose faces lack one of its symbols. if text.chars().any(|c| theme::symbol_fallback(c).is_some()) { theme::available_text(ui, &text) } else { text } } /// A flat square icon button with hover highlight and tooltip. pub fn icon_button(ui: &mut Ui, icon: Icon, color: Color32, tip: &str, size: egui::Vec2) -> Response { let (rect, resp) = ui.allocate_exact_size(size, Sense::click()); let t = Tokens::get(ui.ctx()); if resp.is_pointer_button_down_on() { ui.painter().rect_filled(rect, 4.0, t.pressed); } else if resp.hovered() { ui.painter().rect_filled(rect, 4.0, t.hover); } icons::paint(ui.painter(), Rect::from_center_size(rect.center(), vec2(16.0, 16.0)), icon, color); resp.on_hover_text(tip_text(ui, tip)) } /// A button for a character the interface inserts (the Symbol gallery). When the current font set /// has no glyph for it, the code-drawn icon with the same meaning is painted instead of a tofu /// box. Only the button's appearance changes: clicking still inserts the exact requested character. pub fn glyph_button(ui: &mut Ui, ch: char, tip: &str) -> Response { let font = theme::ui_font(13.0); let has_glyph = ui.ctx().fonts_mut(|f| f.has_glyph(&font, ch)); if !has_glyph && let Some(icon) = icons::fallback_for_symbol(ch) { return icon_button(ui, icon, Tokens::get(ui.ctx()).text, tip, vec2(24.0, 22.0)); } // An unknown future character without a fallback icon still gets a readable label. let label = if has_glyph { ch.to_string() } else { format!("U+{:04X}", ch as u32) }; ui.button(label).on_hover_text(tip_text(ui, tip)) } /// A toggle-able small button (e.g. Bold) showing a checked state. pub fn toggle_button(ui: &mut Ui, icon: Icon, on: bool, tip: &str) -> Response { let (rect, resp) = ui.allocate_exact_size(vec2(26.0, 24.0), Sense::click()); let t = Tokens::get(ui.ctx()); if on { ui.painter().rect_filled(rect, 4.0, t.pressed); ui.painter().rect_stroke(rect, 4.0, Stroke::new(1.0, t.separator), StrokeKind::Inside); } else if resp.hovered() { ui.painter().rect_filled(rect, 4.0, t.hover); } icons::paint(ui.painter(), Rect::from_center_size(rect.center(), vec2(16.0, 16.0)), icon, t.text); resp.on_hover_text(tip_text(ui, tip)) } /// Large ribbon button: 32px icon over a one- or two-line label. pub fn big_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str, dropdown: bool) -> Response { let t = Tokens::get(ui.ctx()); let label = tr(ui, label); let (lines, w) = big_label(ui, label, dropdown); let (rect, resp) = ui.allocate_exact_size(vec2(w, 72.0), Sense::click()); if resp.is_pointer_button_down_on() { ui.painter().rect_filled(rect, 5.0, t.pressed); } else if resp.hovered() { ui.painter().rect_filled(rect, 5.0, t.hover); } icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.center().x, rect.top() + 21.0), vec2(30.0, 30.0)), icon, t.text); paint_big_label(ui, rect, &lines, dropdown); let name = label.replace('\n', " "); let enabled = ui.is_enabled(); resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, &name)); resp.on_hover_text(tip_text(ui, tip)) } /// Large split button (Formulas › AutoSum): the icon runs the default action, the label and its /// arrow open the menu. Returns the (main, arrow) responses; their accessible names are the label /// and "