1337 lines
71 KiB
Rust
1337 lines
71 KiB
Rust
//! The ribbon: tab strip and every tab's groups. Each control runs a command by id.
|
||
|
||
use egui::{Align2, Rect, Sense, Stroke, Ui, pos2, vec2};
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::theme::{Tokens, medium, regular, semibold};
|
||
use crate::widgets::{CONTENT_H, LABEL_H, big, color_grid, combo, group, menu_button, small, split};
|
||
use crate::{WordApp, icons};
|
||
|
||
pub const TABS: [&str; 12] = ["File", "Home", "Insert", "Draw", "Design", "Layout", "References", "Mailings", "Review", "View", "Zotero", "Help"];
|
||
|
||
/// True when the caret/selection touches a picture (#147).
|
||
pub fn has_picture_selected(s: &wordcraft_engine::Session) -> bool {
|
||
matches!(wordcraft_engine::cmd::objects::selected(s), Some((_, wordcraft_doc::para::InlineObject::Image { .. })))
|
||
}
|
||
|
||
/// Contextual tabs for the current selection (pure, tested).
|
||
pub fn contextual_tabs(s: &wordcraft_engine::Session) -> Vec<&'static str> {
|
||
let mut tabs = Vec::new();
|
||
if s.sel.focus.path.cell().is_some() {
|
||
tabs.push("Table Design");
|
||
tabs.push("Table Layout");
|
||
}
|
||
if has_picture_selected(s) {
|
||
tabs.push("Picture Format");
|
||
}
|
||
// Editing an equation.
|
||
if s.math.is_some() {
|
||
tabs.push("Equation");
|
||
}
|
||
tabs
|
||
}
|
||
|
||
/// Tab to show when the stored tab is no longer applicable (pure, tested).
|
||
pub fn resolve_tab<'a>(current: &'a str, available: &[&str]) -> &'a str {
|
||
if available.contains(¤t) { current } else { "Home" }
|
||
}
|
||
|
||
/// Whether the caret is inside a table (so the contextual tabs and their badges are shown).
|
||
pub fn in_table_public(app: &WordApp) -> bool {
|
||
app.session.sel.focus.path.cell().is_some()
|
||
}
|
||
|
||
pub fn show(app: &mut WordApp, ui: &mut Ui) {
|
||
let t = Tokens::get(ui.ctx());
|
||
// Editing an equation brings up the Equation tab; leaving it goes back.
|
||
if app.session.math.is_some() {
|
||
if app.equation_prev_tab.is_none() {
|
||
app.equation_prev_tab = Some(app.ui.tab.clone());
|
||
app.ui.tab = "Equation".into();
|
||
}
|
||
} else if let Some(prev) = app.equation_prev_tab.take()
|
||
&& app.ui.tab == "Equation"
|
||
{
|
||
app.ui.tab = prev;
|
||
}
|
||
// A contextual tab (Table, Picture Format) may be stored while the selection moved away.
|
||
{
|
||
let mut tabs: Vec<&str> = TABS.to_vec();
|
||
tabs.extend(contextual_tabs(&app.session));
|
||
let next = resolve_tab(&app.ui.tab, &tabs);
|
||
if next != app.ui.tab {
|
||
app.ui.tab = next.to_string();
|
||
}
|
||
}
|
||
// Tab strip.
|
||
egui::Panel::top("tabs")
|
||
.exact_size(30.0)
|
||
.frame(egui::Frame::NONE.fill(t.tab_strip).inner_margin(egui::Margin { left: 8, right: 10, top: 0, bottom: 0 }))
|
||
.show(ui, |ui| {
|
||
ui.horizontal_centered(|ui| {
|
||
ui.spacing_mut().item_spacing = vec2(2.0, 0.0);
|
||
let mut tabs: Vec<&str> = TABS.to_vec();
|
||
for ct in contextual_tabs(&app.session) {
|
||
if !tabs.contains(&ct) {
|
||
tabs.push(ct);
|
||
}
|
||
}
|
||
for tab in tabs {
|
||
let contextual = tab.starts_with("Table ") || tab == "Picture Format" || tab == "Equation";
|
||
let shown = tl!(tab);
|
||
let w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(shown.to_string(), medium(12.5), t.text).size().x) + 18.0;
|
||
let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click());
|
||
if app.ui.keytips == crate::keytips::Phase::Tabs {
|
||
crate::keytips::record_tab(r, tab, &mut app.keytip_rects);
|
||
}
|
||
let active = app.ui.tab == tab && !app.ui.backstage;
|
||
if resp.hovered() && !active {
|
||
ui.painter().rect_filled(r.shrink2(vec2(0.0, 4.0)), 4.0, t.hover);
|
||
}
|
||
let color = if contextual || active { t.accent_text } else { t.text };
|
||
ui.painter().text(r.center(), Align2::CENTER_CENTER, shown, if active { semibold(12.5) } else { medium(12.5) }, color);
|
||
if active {
|
||
let u = Rect::from_center_size(pos2(r.center().x, r.max.y - 2.0), vec2(w - 16.0, 3.0));
|
||
ui.painter().rect_filled(u, 2.0, t.accent);
|
||
}
|
||
if resp.clicked() {
|
||
let _ = app.run("ui.tab", json!({"tab": tab}));
|
||
}
|
||
if resp.double_clicked() && tab != "File" {
|
||
let _ = app.run("ui.collapseRibbon", json!({}));
|
||
}
|
||
}
|
||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||
// Share, Editing mode, Comments.
|
||
let share = tl!("Share");
|
||
let share_w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(share.to_string(), medium(12.0), t.text).size().x);
|
||
let (r, resp) = ui.allocate_exact_size(vec2((share_w + 36.0).max(74.0), 24.0), Sense::click());
|
||
ui.painter().rect_filled(r, 4.0, if resp.hovered() { t.accent_text } else { t.accent });
|
||
icons::paint(
|
||
ui.painter(),
|
||
Rect::from_center_size(pos2(r.min.x + 14.0, r.center().y), vec2(14.0, 14.0)),
|
||
"share",
|
||
egui::Color32::WHITE,
|
||
egui::Color32::WHITE,
|
||
);
|
||
ui.painter().text(pos2(r.min.x + 26.0, r.center().y), Align2::LEFT_CENTER, share, medium(12.0), egui::Color32::WHITE);
|
||
if resp.on_hover_text(tl!("Export a copy to share (PDF, Word document)")).clicked() {
|
||
let _ = app.run("ui.backstage", json!({"value": true, "page": "export"}));
|
||
}
|
||
ui.add_space(6.0);
|
||
let track = app.session.doc.settings.track_changes;
|
||
ui.menu_button(
|
||
egui::RichText::new(format!("✎ {} ▾", if track { tl!("Reviewing") } else { tl!("Editing") })).font(regular(12.0)),
|
||
|ui| {
|
||
if ui.selectable_label(!track, tl!(tl!("Editing — edit the document directly"))).clicked() {
|
||
let _ = app.run("review.trackChanges", json!({"value": false}));
|
||
ui.close();
|
||
}
|
||
if ui.selectable_label(track, tl!(tl!("Reviewing — edits become suggestions"))).clicked() {
|
||
let _ = app.run("review.trackChanges", json!({"value": true}));
|
||
ui.close();
|
||
}
|
||
},
|
||
);
|
||
ui.add_space(4.0);
|
||
if ui.button(egui::RichText::new(format!("💬 {}", tl!("Comments"))).font(regular(12.0))).clicked() {
|
||
let _ = app.run("view.commentsPane", json!({}));
|
||
}
|
||
});
|
||
});
|
||
});
|
||
if app.ui.ribbon_collapsed {
|
||
return;
|
||
}
|
||
egui::Panel::top("ribbon")
|
||
.exact_size(CONTENT_H + LABEL_H + 10.0)
|
||
.frame(
|
||
egui::Frame::NONE.fill(t.ribbon).inner_margin(egui::Margin { left: 8, right: 8, top: 4, bottom: 4 }).stroke(Stroke::new(1.0, t.border)),
|
||
)
|
||
.show(ui, |ui| {
|
||
let scroll = egui::ScrollArea::horizontal().scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden).show(ui, |ui| {
|
||
ui.horizontal_top(|ui| {
|
||
ui.spacing_mut().item_spacing = vec2(2.0, 2.0);
|
||
match app.ui.tab.as_str() {
|
||
"Home" => home(app, ui),
|
||
"Insert" => insert(app, ui),
|
||
"Draw" => draw(app, ui),
|
||
"Design" => design(app, ui),
|
||
"Layout" => layout(app, ui),
|
||
"References" => references(app, ui),
|
||
"Mailings" => mailings(app, ui),
|
||
"Review" => review(app, ui),
|
||
"View" => view(app, ui),
|
||
"Zotero" => zotero(app, ui),
|
||
"Help" => help(app, ui),
|
||
"Table Design" => table_design(app, ui),
|
||
"Table Layout" => table_layout(app, ui),
|
||
"Equation" if app.session.math.is_some() => crate::equation_tab::show(app, ui),
|
||
"Picture Format" => picture_format(app, ui),
|
||
_ => home(app, ui),
|
||
}
|
||
});
|
||
});
|
||
// The bar is hidden, so a row wider than the window would be clipped with no sign. Edge chevrons show
|
||
// where more is and scroll it on click. Every command also stays reachable through Search Commands.
|
||
let r = scroll.inner_rect;
|
||
let overflow = scroll.content_size.x - r.width();
|
||
let offset = scroll.state.offset.x;
|
||
let step = 160.0;
|
||
let mut next = None;
|
||
if offset > 1.0 {
|
||
let left = Rect::from_min_max(r.left_top(), pos2(r.left() + 18.0, r.bottom()));
|
||
if edge_cue(ui, left, "‹", tl!("Scroll ribbon left"), &t) {
|
||
next = Some(offset - step);
|
||
}
|
||
}
|
||
if overflow - offset > 1.0 {
|
||
let right = Rect::from_min_max(pos2(r.right() - 18.0, r.top()), r.right_bottom());
|
||
if edge_cue(ui, right, "›", tl!("Scroll ribbon right"), &t) {
|
||
next = Some(offset + step);
|
||
}
|
||
}
|
||
if let Some(x) = next {
|
||
let mut state = scroll.state;
|
||
state.offset.x = x.clamp(0.0, overflow.max(0.0));
|
||
state.store(ui.ctx(), scroll.id);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// A chevron over a ribbon edge that is also a button; `true` on the frame it is clicked.
|
||
fn edge_cue(ui: &mut Ui, rect: Rect, glyph: &str, hint: &str, t: &Tokens) -> bool {
|
||
let resp = ui.interact(rect, ui.id().with(glyph), Sense::click()).on_hover_text(hint);
|
||
let ink = if resp.hovered() { t.text } else { t.text_dim };
|
||
ui.painter().rect_filled(rect, 0.0, t.ribbon);
|
||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, glyph, egui::FontId::proportional(18.0), ink);
|
||
resp.clicked()
|
||
}
|
||
|
||
fn stack(ui: &mut Ui, add: impl FnOnce(&mut Ui)) {
|
||
ui.vertical(|ui| {
|
||
ui.spacing_mut().item_spacing = vec2(1.0, 0.0);
|
||
add(ui);
|
||
});
|
||
}
|
||
|
||
fn mi(ui: &mut Ui, app: &mut WordApp, label: &str, id: &str, params: Value) {
|
||
let sc = crate::widgets::shortcut_text(app, id);
|
||
let enabled = crate::widgets::enabled(app, id);
|
||
let resp = ui.add_enabled(enabled, egui::Button::new(tl!(label)).shortcut_text(sc).min_size(vec2(200.0, 0.0)));
|
||
if resp.clicked() {
|
||
let _ = app.run(id, params);
|
||
ui.close();
|
||
}
|
||
}
|
||
|
||
fn home(app: &mut WordApp, ui: &mut Ui) {
|
||
let st = app.session.run("format.state", &json!({})).unwrap_or_default();
|
||
let flag = |k: &str| st.get(k).and_then(Value::as_bool).unwrap_or(false);
|
||
group(ui, "Clipboard", None, app, |ui, app| {
|
||
menu_button(ui, app, "paste", Some("Paste"), "Paste (⌘V)", true, |ui, app| {
|
||
mi(ui, app, "Paste", "edit.paste", json!({}));
|
||
mi(ui, app, "Keep Text Only", "edit.pasteText", json!({}));
|
||
mi(ui, app, "Merge Formatting", "edit.pasteMerge", json!({}));
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "cut", Some("Cut"), "Cut", "edit.cut", json!({}), false);
|
||
let r = small(ui, app, "copy", Some("Copy"), "Copy", "edit.copy", json!({}), false);
|
||
if r.clicked() {
|
||
ui.ctx().copy_text(app.session.clipboard_text.clone());
|
||
}
|
||
let active = app.session.painter.is_some();
|
||
small(ui, app, "painter", Some("Format"), "Format Painter", "edit.formatPainter", json!({}), active);
|
||
});
|
||
});
|
||
group(ui, "Font", Some("format.fontDialog"), app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
crate::widgets::row(ui, |ui| {
|
||
let font = st.get("font").and_then(Value::as_str).unwrap_or("").to_string();
|
||
let fams = app.previews.families();
|
||
let prev = app.previews.font_preview_fn();
|
||
if let Some(f) = combo(ui, "font", 150.0, &font, &fams, Some(&*prev)) {
|
||
let _ = app.run("format.font", json!({"name": f}));
|
||
}
|
||
let size = st
|
||
.get("size")
|
||
.and_then(Value::as_f64)
|
||
.map(|s| if s.fract() == 0.0 { format!("{s:.0}") } else { format!("{s}") })
|
||
.unwrap_or_default();
|
||
let sizes: Vec<String> =
|
||
wordcraft_engine::cmd::format::SIZES.iter().map(|s| if s.fract() == 0.0 { format!("{s:.0}") } else { s.to_string() }).collect();
|
||
if let Some(v) = combo(ui, "size", 52.0, &size, &sizes, None)
|
||
&& let Ok(x) = v.trim().parse::<f64>()
|
||
{
|
||
let _ = app.run("format.size", json!({"size": x}));
|
||
}
|
||
ui.add_space(3.0);
|
||
small(ui, app, "grow", None, "Increase Font Size", "format.growFont", json!({}), false);
|
||
small(ui, app, "shrink", None, "Decrease Font Size", "format.shrinkFont", json!({}), false);
|
||
menu_button(ui, app, "case", None, "Change Case", false, |ui, app| {
|
||
mi(ui, app, "Sentence case.", "format.changeCase", json!({"mode": "sentence"}));
|
||
mi(ui, app, "lowercase", "format.changeCase", json!({"mode": "lower"}));
|
||
mi(ui, app, "UPPERCASE", "format.changeCase", json!({"mode": "upper"}));
|
||
mi(ui, app, "Capitalize Each Word", "format.changeCase", json!({"mode": "title"}));
|
||
mi(ui, app, "tOGGLE cASE", "format.changeCase", json!({"mode": "toggle"}));
|
||
});
|
||
small(ui, app, "clear", None, "Clear All Formatting", "format.clear", json!({}), false);
|
||
});
|
||
ui.add_space(3.0);
|
||
crate::widgets::row(ui, |ui| {
|
||
small(ui, app, "bold", None, "Bold", "format.bold", json!({}), flag("bold"));
|
||
small(ui, app, "italic", None, "Italic", "format.italic", json!({}), flag("italic"));
|
||
split(ui, app, "underline", "Underline", "format.underline", json!({}), flag("underline"), None, |ui, app| {
|
||
for (l, s) in [
|
||
("Single", "single"),
|
||
("Double", "double"),
|
||
("Thick", "thick"),
|
||
("Dotted", "dotted"),
|
||
("Dashed", "dash"),
|
||
("Dot-dash", "dotDash"),
|
||
("Wave", "wave"),
|
||
("Words only", "words"),
|
||
] {
|
||
mi(ui, app, l, "format.underline", json!({"style": s}));
|
||
}
|
||
});
|
||
small(ui, app, "strike", None, "Strikethrough", "format.strikethrough", json!({}), flag("strike"));
|
||
small(ui, app, "subscript", None, "Subscript", "format.subscript", json!({}), flag("subscript"));
|
||
small(ui, app, "superscript", None, "Superscript", "format.superscript", json!({}), flag("superscript"));
|
||
small(ui, app, "charborder", None, "Character Border", "format.border", json!({}), flag("border"));
|
||
ui.add_space(4.0);
|
||
menu_button(ui, app, "effects", None, "Text Effects and Typography", false, |ui, app| {
|
||
mi(ui, app, "Outline", "format.outline", json!({}));
|
||
mi(ui, app, "Shadow", "format.shadow", json!({}));
|
||
mi(ui, app, "Small Caps", "format.smallCaps", json!({}));
|
||
mi(ui, app, "All Caps", "format.allCaps", json!({}));
|
||
mi(ui, app, "Double Strikethrough", "format.doubleStrikethrough", json!({}));
|
||
});
|
||
let hl = app.canvas.last_highlight.clone();
|
||
split(ui, app, "highlight", "Text Highlight Color", "format.highlight", json!({"color": hl}), false, None, |ui, app| {
|
||
let grid = wordcraft_doc::props::Highlight::ALL;
|
||
egui::Grid::new("hl").spacing(vec2(3.0, 3.0)).show(ui, |ui| {
|
||
for (i, h) in grid.iter().enumerate().skip(1) {
|
||
let c = h.rgb().unwrap_or(wordcraft_doc::Rgb::WHITE);
|
||
let (r, resp) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::click());
|
||
ui.painter().rect_filled(r, 2.0, egui::Color32::from_rgb(c.0, c.1, c.2));
|
||
if resp.on_hover_text(h.name()).clicked() {
|
||
app.canvas.last_highlight = h.ooxml().to_string();
|
||
let _ = app.run("format.highlight", json!({"color": h.ooxml()}));
|
||
ui.close();
|
||
}
|
||
if i % 5 == 0 {
|
||
ui.end_row();
|
||
}
|
||
}
|
||
});
|
||
mi(ui, app, "No Color", "format.highlight", json!({"color": "none"}));
|
||
});
|
||
let fc = app.canvas.last_font_color.clone();
|
||
let sw = wordcraft_doc::Rgb::parse(&fc).map(crate::theme::c32);
|
||
split(ui, app, "fontcolor", "Font Color", "format.color", json!({"color": fc}), false, sw, |ui, app| {
|
||
mi(ui, app, "Automatic", "format.color", json!({"color": "auto"}));
|
||
let theme = app.session.doc.settings.theme_colors.clone();
|
||
if let Some(hex) = color_grid(ui, &theme) {
|
||
app.canvas.last_font_color = hex.clone();
|
||
let _ = app.run("format.color", json!({"color": hex}));
|
||
ui.close();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Paragraph", Some("para.dialog"), app, |ui, app| {
|
||
let rp = app.session.doc.para_at(&app.session.sel.focus).map(|p| app.session.doc.styles.resolve_para(&p.props));
|
||
let align = rp.as_ref().map(|r| r.align).unwrap_or_default();
|
||
stack(ui, |ui| {
|
||
crate::widgets::row(ui, |ui| {
|
||
split(ui, app, "bullets", "Bullets", "para.bullets", json!({}), false, None, |ui, app| {
|
||
ui.label(egui::RichText::new(tl!("Bullet Library")).small().weak());
|
||
ui.horizontal(|ui| {
|
||
for c in ["•", "○", "▪", "◆", "➢", "✓", "–"] {
|
||
if ui.button(egui::RichText::new(c).size(16.0)).clicked() {
|
||
let _ = app.run("para.bullets", json!({"kind": c}));
|
||
ui.close();
|
||
}
|
||
}
|
||
});
|
||
mi(ui, app, "None", "para.bullets", json!({"off": true}));
|
||
});
|
||
split(ui, app, "numbering", "Numbering", "para.numbering", json!({}), false, None, |ui, app| {
|
||
for (l, k) in [
|
||
("1. 2. 3.", "numbered"),
|
||
("1) 2) 3)", "numberedParen"),
|
||
("I. II. III.", "outline"),
|
||
("A. B. C.", "upperLetter"),
|
||
("a) b) c)", "lowerLetter"),
|
||
("i. ii. iii.", "lowerRoman"),
|
||
] {
|
||
mi(ui, app, l, "para.numbering", json!({"kind": k}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Restart at 1", "para.restartNumbering", json!({}));
|
||
mi(ui, app, "None", "para.numbering", json!({"off": true}));
|
||
});
|
||
split(ui, app, "multilevel", "Multilevel List", "para.multilevel", json!({}), false, None, |ui, app| {
|
||
mi(ui, app, "1. 1.1. 1.1.1.", "para.multilevel", json!({"kind": "legal"}));
|
||
mi(ui, app, "I. A. 1. a.", "para.multilevel", json!({"kind": "outline"}));
|
||
ui.separator();
|
||
for lv in 0..5u64 {
|
||
mi(ui, app, &format!("Change to Level {}", lv + 1), "para.listLevel", json!({"level": lv}));
|
||
}
|
||
});
|
||
ui.add_space(4.0);
|
||
small(ui, app, "outdent", None, "Decrease Indent", "para.outdent", json!({}), false);
|
||
small(ui, app, "indent", None, "Increase Indent", "para.indent", json!({}), false);
|
||
ui.add_space(4.0);
|
||
small(ui, app, "sort", None, "Sort", "para.sort", json!({}), false);
|
||
let marks = app.session.view.marks;
|
||
small(ui, app, "pilcrow", None, "Show/Hide ¶", "view.marks", json!({}), marks);
|
||
});
|
||
ui.add_space(3.0);
|
||
crate::widgets::row(ui, |ui| {
|
||
use wordcraft_doc::Align as A;
|
||
small(ui, app, "alignLeft", None, "Align Left", "para.alignLeft", json!({}), align == A::Left);
|
||
small(ui, app, "alignCenter", None, "Center", "para.alignCenter", json!({}), align == A::Center);
|
||
small(ui, app, "alignRight", None, "Align Right", "para.alignRight", json!({}), align == A::Right);
|
||
small(ui, app, "justify", None, "Justify", "para.justify", json!({}), align == A::Justify);
|
||
ui.add_space(4.0);
|
||
menu_button(ui, app, "lineSpacing", None, "Line and Paragraph Spacing", false, |ui, app| {
|
||
for v in [1.0, 1.15, 1.5, 2.0, 2.5, 3.0] {
|
||
mi(ui, app, &format!("{v}"), "para.lineSpacing", json!({"value": v}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Add Space Before Paragraph", "para.addSpaceBefore", json!({}));
|
||
mi(ui, app, "Remove Space After Paragraph", "para.removeSpaceAfter", json!({}));
|
||
mi(ui, app, "Line Spacing Options…", "para.dialog", json!({}));
|
||
});
|
||
split(ui, app, "shading", "Shading", "para.shading", json!({"color": app.canvas.last_shading.clone()}), false, None, |ui, app| {
|
||
mi(ui, app, "No Color", "para.shading", json!({"color": null}));
|
||
let theme = app.session.doc.settings.theme_colors.clone();
|
||
if let Some(hex) = color_grid(ui, &theme) {
|
||
app.canvas.last_shading = hex.clone();
|
||
let _ = app.run("para.shading", json!({"color": hex}));
|
||
ui.close();
|
||
}
|
||
});
|
||
split(ui, app, "borders", "Borders", "para.borders", json!({"kind": "bottom"}), false, None, |ui, app| {
|
||
for (l, k) in [
|
||
("Bottom Border", "bottom"),
|
||
("Top Border", "top"),
|
||
("Left Border", "left"),
|
||
("Right Border", "right"),
|
||
("No Border", "none"),
|
||
("All Borders", "all"),
|
||
("Outside Borders", "outside"),
|
||
("Inside Borders", "inside"),
|
||
] {
|
||
mi(ui, app, l, "para.borders", json!({"kind": k}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Horizontal Line", "insert.horizontalLine", json!({}));
|
||
});
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Styles", Some("styles.pane"), app, |ui, app| {
|
||
crate::previews::style_gallery(app, ui, &st);
|
||
});
|
||
group(ui, "Editing", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
small(ui, app, "find", Some("Find"), "Find", "ui.dialog", json!({"name": "find"}), false).clicked();
|
||
small(ui, app, "replace", Some("Replace"), "Replace", "ui.dialog", json!({"name": "replace"}), false);
|
||
menu_button(ui, app, "select", Some("Select"), "Select", false, |ui, app| {
|
||
mi(ui, app, "Select All", "select.all", json!({}));
|
||
mi(ui, app, "Select Paragraph", "select.paragraph", json!({}));
|
||
mi(ui, app, "Select Sentence", "select.sentence", json!({}));
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Voice", None, app, |ui, app| {
|
||
big(ui, app, "dictate", "Dictate", "tools.dictate", json!({}), false);
|
||
});
|
||
group(ui, "Editor", None, app, |ui, app| {
|
||
big(ui, app, "editor", "Editor", "review.spelling", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn insert(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Pages", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "coverPage", Some("Cover Page"), "Cover Page", false, |ui, app| {
|
||
mi(ui, app, "Studio cover", "insert.coverPage", json!({}));
|
||
});
|
||
small(ui, app, "blankPage", Some("Blank Page"), "Blank Page", "insert.blankPage", json!({}), false);
|
||
small(ui, app, "pageBreak", Some("Page Break"), "Page Break", "insert.pageBreak", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Tables", None, app, |ui, app| {
|
||
menu_button(ui, app, "table", Some("Table"), "Add a Table", true, |ui, app| {
|
||
crate::dialogs::table_grid_picker(ui, app);
|
||
ui.separator();
|
||
mi(ui, app, "Insert Table…", "ui.dialog", json!({"name": "insertTable"}));
|
||
mi(ui, app, "Convert Text to Table…", "table.fromText", json!({}));
|
||
ui.menu_button(tl!("Quick Tables"), |ui| {
|
||
mi(ui, app, "Tabular List", "table.quick", json!({"kind": "tabular"}));
|
||
mi(ui, app, "Matrix", "table.quick", json!({"kind": "matrix"}));
|
||
mi(ui, app, "Calendar", "table.quick", json!({"kind": "calendar"}));
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Illustrations", None, app, |ui, app| {
|
||
big(ui, app, "picture", "Pictures", "insert.picture", json!({}), false);
|
||
menu_button(ui, app, "shapes", Some("Shapes"), "Shapes", true, |ui, app| {
|
||
for (l, k) in [
|
||
("Rectangle", "rectangle"),
|
||
("Rounded Rectangle", "roundedRectangle"),
|
||
("Oval", "ellipse"),
|
||
("Triangle", "triangle"),
|
||
("Diamond", "diamond"),
|
||
("Line", "line"),
|
||
("Arrow", "arrow"),
|
||
("Star", "star"),
|
||
("Heart", "heart"),
|
||
] {
|
||
mi(ui, app, l, "insert.shape", json!({"kind": k}));
|
||
}
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "icons", Some("Icons"), "Icons", "insert.icon", json!({}), false);
|
||
small(ui, app, "smartArt", Some("SmartArt"), "SmartArt", "insert.smartArt", json!({}), false);
|
||
small(ui, app, "chart", Some("Chart"), "Chart", "insert.chart", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Links", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
small(ui, app, "link", Some("Link"), "Link", "ui.dialog", json!({"name": "link"}), false);
|
||
small(ui, app, "bookmark", Some("Bookmark"), "Bookmark", "ui.dialog", json!({"name": "bookmark"}), false);
|
||
small(ui, app, "crossRef", Some("Cross-reference"), "Cross-reference", "insert.crossReference", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Comments", None, app, |ui, app| {
|
||
big(ui, app, "newComment", "Comment", "review.newComment", json!({}), false);
|
||
});
|
||
group(ui, "Header & Footer", None, app, |ui, app| {
|
||
menu_button(ui, app, "header", Some("Header"), "Header", true, |ui, app| {
|
||
mi(ui, app, "Blank", "insert.header", json!({"preset": "blank"}));
|
||
mi(ui, app, "Blank (Three Columns)", "insert.header", json!({"preset": "blankThree"}));
|
||
mi(ui, app, "Document Title", "insert.header", json!({"preset": "title"}));
|
||
ui.separator();
|
||
mi(ui, app, "Edit Header", "insert.editHeader", json!({}));
|
||
mi(ui, app, "Remove Header", "insert.removeHeader", json!({}));
|
||
});
|
||
menu_button(ui, app, "footer", Some("Footer"), "Footer", true, |ui, app| {
|
||
mi(ui, app, "Blank", "insert.footer", json!({"preset": "blank"}));
|
||
mi(ui, app, "Blank (Three Columns)", "insert.footer", json!({"preset": "blankThree"}));
|
||
mi(ui, app, "Page Number", "insert.footer", json!({"preset": "pageNumber"}));
|
||
ui.separator();
|
||
mi(ui, app, "Edit Footer", "insert.editFooter", json!({}));
|
||
mi(ui, app, "Remove Footer", "insert.removeFooter", json!({}));
|
||
});
|
||
menu_button(ui, app, "pageNumber", Some("Page\nNumber"), "Page Number", true, |ui, app| {
|
||
mi(ui, app, "Top of Page", "insert.pageNumber", json!({"position": "top", "align": "right"}));
|
||
mi(ui, app, "Bottom of Page", "insert.pageNumber", json!({"position": "bottom", "align": "center"}));
|
||
mi(ui, app, "Page X of Y", "insert.pageNumber", json!({"position": "bottom", "format": "x of y"}));
|
||
mi(ui, app, "Current Position", "insert.pageNumber", json!({"position": "current"}));
|
||
mi(ui, app, "Format Page Numbers…", "layout.pageNumberFormat", json!({}));
|
||
});
|
||
});
|
||
group(ui, "Text", None, app, |ui, app| {
|
||
big(ui, app, "textBox", "Text\nBox", "insert.textBox", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "quickParts", Some("Quick Parts"), "Quick Parts", "insert.quickParts", json!({}), false);
|
||
small(ui, app, "wordArt", Some("WordArt"), "WordArt", "insert.wordArt", json!({}), false);
|
||
small(ui, app, "dropCap", Some("Drop Cap"), "Drop Cap", "insert.dropCap", json!({}), false);
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "signature", None, "Signature Line", "insert.signatureLine", json!({}), false);
|
||
small(ui, app, "dateTime", None, "Date & Time", "insert.dateTime", json!({}), false);
|
||
small(ui, app, "object", None, "Object", "insert.object", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Symbols", None, app, |ui, app| {
|
||
crate::equation_tab::insert_button(ui, app);
|
||
menu_button(ui, app, "symbol", Some("Symbol"), "Symbol", true, |ui, app| {
|
||
egui::Grid::new("syms").show(ui, |ui| {
|
||
for (i, c) in [
|
||
"©", "®", "™", "§", "¶", "€", "£", "¥", "°", "±", "≠", "≤", "≥", "÷", "×", "∞", "µ", "α", "β", "π", "Ω", "∑", "√", "→", "←", "✓",
|
||
"★", "♥", "—", "…",
|
||
]
|
||
.iter()
|
||
.enumerate()
|
||
{
|
||
if ui.button(egui::RichText::new(*c).size(16.0)).clicked() {
|
||
let _ = app.run("insert.symbol", json!({"char": c}));
|
||
ui.close();
|
||
}
|
||
if i % 6 == 5 {
|
||
ui.end_row();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
fn draw(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Drawing Tools", None, app, |ui, app| {
|
||
big(ui, app, "select", "Select", "draw.select", json!({}), false);
|
||
big(ui, app, "lasso", "Lasso", "draw.lasso", json!({}), false);
|
||
big(ui, app, "eraser", "Eraser", "draw.eraser", json!({}), false);
|
||
big(ui, app, "pen", "Pen", "draw.pen", json!({}), false);
|
||
big(ui, app, "pencil", "Pencil", "draw.pencil", json!({}), false);
|
||
big(ui, app, "highlight", "Highlighter", "draw.highlighter", json!({}), false);
|
||
});
|
||
group(ui, "Convert", None, app, |ui, app| {
|
||
big(ui, app, "inkToShape", "Ink to\nShape", "draw.inkToShape", json!({}), false);
|
||
big(ui, app, "inkToMath", "Ink to\nMath", "draw.inkToMath", json!({}), false);
|
||
});
|
||
group(ui, "Insert", None, app, |ui, app| {
|
||
big(ui, app, "canvas", "Drawing\nCanvas", "insert.canvas", json!({}), false);
|
||
});
|
||
group(ui, "Replay", None, app, |ui, app| {
|
||
big(ui, app, "replay", "Ink\nReplay", "draw.replay", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn design(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Themes", None, app, |ui, app| {
|
||
menu_button(ui, app, "themes", Some("Themes"), "Themes", true, |ui, app| {
|
||
for (name, ..) in wordcraft_engine::cmd::design::THEMES {
|
||
mi(ui, app, name, "design.theme", json!({"name": name}));
|
||
}
|
||
});
|
||
});
|
||
group(ui, "Document Formatting", None, app, |ui, app| {
|
||
crate::previews::style_set_gallery(app, ui);
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "colors", Some("Colors"), "Theme Colors", false, |ui, app| {
|
||
for (name, ..) in wordcraft_engine::cmd::design::THEMES {
|
||
mi(ui, app, name, "design.themeColors", json!({"name": name}));
|
||
}
|
||
});
|
||
menu_button(ui, app, "fonts", Some("Fonts"), "Theme Fonts", false, |ui, app| {
|
||
for (name, h, b, _) in wordcraft_engine::cmd::design::THEMES {
|
||
mi(ui, app, &format!("{name}: {h} / {b}"), "design.themeFonts", json!({"heading": h, "body": b}));
|
||
}
|
||
});
|
||
});
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "paraSpacing", Some("Paragraph Spacing"), "Paragraph Spacing", false, |ui, app| {
|
||
for (l, v) in [
|
||
("Default", "default"),
|
||
("No Paragraph Space", "none"),
|
||
("Compact", "compact"),
|
||
("Tight", "tight"),
|
||
("Open", "open"),
|
||
("Relaxed", "relaxed"),
|
||
("Double", "double"),
|
||
] {
|
||
mi(ui, app, l, "design.paragraphSpacing", json!({"value": v}));
|
||
}
|
||
});
|
||
small(ui, app, "effectsDesign", Some("Effects"), "Effects", "design.effects", json!({}), false);
|
||
small(ui, app, "setDefault", Some("Set as Default"), "Set as Default", "design.setDefault", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Page Background", None, app, |ui, app| {
|
||
menu_button(ui, app, "watermark", Some("Watermark"), "Watermark", true, |ui, app| {
|
||
for w in ["DRAFT", "CONFIDENTIAL", "DO NOT COPY", "SAMPLE", "ASAP", "URGENT"] {
|
||
mi(ui, app, w, "design.watermark", json!({"text": w}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Custom Watermark…", "ui.dialog", json!({"name": "watermark"}));
|
||
mi(ui, app, "Remove Watermark", "design.watermark", json!({"remove": true}));
|
||
});
|
||
menu_button(ui, app, "pageColor", Some("Page\nColor"), "Page Color", true, |ui, app| {
|
||
mi(ui, app, "No Color", "design.pageColor", json!({"color": null}));
|
||
let theme = app.session.doc.settings.theme_colors.clone();
|
||
if let Some(hex) = color_grid(ui, &theme) {
|
||
let _ = app.run("design.pageColor", json!({"color": hex}));
|
||
ui.close();
|
||
}
|
||
});
|
||
menu_button(ui, app, "pageBorders", Some("Page\nBorders"), "Page Borders", true, |ui, app| {
|
||
mi(ui, app, "Box", "design.pageBorders", json!({"kind": "box"}));
|
||
mi(ui, app, "None", "design.pageBorders", json!({"kind": "none"}));
|
||
});
|
||
});
|
||
}
|
||
|
||
fn layout(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Page Setup", Some("ui.dialog"), app, |ui, app| {
|
||
menu_button(ui, app, "margins", Some("Margins"), "Margins", true, |ui, app| {
|
||
for (l, k) in [
|
||
("Normal 1\" all", "normal"),
|
||
("Narrow 0.5\" all", "narrow"),
|
||
("Moderate", "moderate"),
|
||
("Wide", "wide"),
|
||
("Mirrored", "mirrored"),
|
||
("Office 2003 Default", "office2003"),
|
||
] {
|
||
mi(ui, app, l, "layout.margins", json!({"preset": k}));
|
||
}
|
||
mi(ui, app, "Custom Margins…", "ui.dialog", json!({"name": "pageSetup"}));
|
||
});
|
||
menu_button(ui, app, "orientation", Some("Orientation"), "Orientation", true, |ui, app| {
|
||
mi(ui, app, "Portrait", "layout.orientation", json!({"value": "portrait"}));
|
||
mi(ui, app, "Landscape", "layout.orientation", json!({"value": "landscape"}));
|
||
});
|
||
menu_button(ui, app, "size", Some("Size"), "Size", true, |ui, app| {
|
||
for (n, w, h) in wordcraft_geom::PAPER_SIZES {
|
||
mi(ui, app, &format!("{n} {:.2}\" × {:.2}\"", w / 72.0, h / 72.0), "layout.size", json!({"name": n}));
|
||
}
|
||
});
|
||
menu_button(ui, app, "columns", Some("Columns"), "Columns", true, |ui, app| {
|
||
mi(ui, app, "One", "layout.columns", json!({"count": 1}));
|
||
mi(ui, app, "Two", "layout.columns", json!({"count": 2}));
|
||
mi(ui, app, "Three", "layout.columns", json!({"count": 3}));
|
||
mi(ui, app, "Left", "layout.columns", json!({"preset": "left"}));
|
||
mi(ui, app, "Right", "layout.columns", json!({"preset": "right"}));
|
||
});
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "breaks", Some("Breaks"), "Breaks", false, |ui, app| {
|
||
ui.label(egui::RichText::new(tl!("Page Breaks")).strong());
|
||
mi(ui, app, "Page", "layout.break", json!({"kind": "page"}));
|
||
mi(ui, app, "Column", "layout.break", json!({"kind": "column"}));
|
||
mi(ui, app, "Text Wrapping", "layout.break", json!({"kind": "textWrapping"}));
|
||
ui.label(egui::RichText::new(tl!("Section Breaks")).strong());
|
||
mi(ui, app, "Next Page", "layout.break", json!({"kind": "nextPage"}));
|
||
mi(ui, app, "Continuous", "layout.break", json!({"kind": "continuous"}));
|
||
mi(ui, app, "Even Page", "layout.break", json!({"kind": "evenPage"}));
|
||
mi(ui, app, "Odd Page", "layout.break", json!({"kind": "oddPage"}));
|
||
});
|
||
menu_button(ui, app, "lineNumbers", Some("Line Numbers"), "Line Numbers", false, |ui, app| {
|
||
for (l, v) in
|
||
[("None", "none"), ("Continuous", "continuous"), ("Restart Each Page", "restartPage"), ("Restart Each Section", "restartSection")]
|
||
{
|
||
mi(ui, app, l, "layout.lineNumbers", json!({"value": v}));
|
||
}
|
||
});
|
||
let hy = app.session.doc.settings.auto_hyphenation;
|
||
menu_button(ui, app, "hyphenation", Some("Hyphenation"), "Hyphenation", false, |ui, app| {
|
||
mi(ui, app, if hy { "✓ Automatic" } else { "Automatic" }, "layout.hyphenation", json!({"value": true}));
|
||
mi(ui, app, if hy { "None" } else { "✓ None" }, "layout.hyphenation", json!({"value": false}));
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Paragraph", Some("para.dialog"), app, |ui, app| {
|
||
let rp = app.session.doc.para_at(&app.session.sel.focus).map(|p| app.session.doc.styles.resolve_para(&p.props));
|
||
let Some(rp) = rp else { return };
|
||
egui::Grid::new("layout_para").spacing(vec2(6.0, 4.0)).show(ui, |ui| {
|
||
ui.label(egui::RichText::new(tl!("Indent")).small().strong());
|
||
ui.label("");
|
||
ui.label(egui::RichText::new(tl!("Spacing")).small().strong());
|
||
ui.end_row();
|
||
let mut l = rp.indent_left / 72.0;
|
||
let mut b = rp.space_before;
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(tl!("Left:")).small());
|
||
if ui.add(egui::DragValue::new(&mut l).speed(0.05).range(-11.0..=22.0).suffix("\"").max_decimals(2)).changed() {
|
||
let _ = app.run("para.indents", json!({"left": l * 72.0}));
|
||
}
|
||
});
|
||
ui.label("");
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(tl!("Before:")).small());
|
||
if ui.add(egui::DragValue::new(&mut b).speed(1.0).range(0.0..=1584.0).suffix(" pt")).changed() {
|
||
let _ = app.run("para.spacing", json!({"before": b}));
|
||
}
|
||
});
|
||
ui.end_row();
|
||
let mut r = rp.indent_right / 72.0;
|
||
let mut a = rp.space_after;
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(tl!("Right:")).small());
|
||
if ui.add(egui::DragValue::new(&mut r).speed(0.05).range(-11.0..=22.0).suffix("\"").max_decimals(2)).changed() {
|
||
let _ = app.run("para.indents", json!({"right": r * 72.0}));
|
||
}
|
||
});
|
||
ui.label("");
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(tl!("After:")).small());
|
||
if ui.add(egui::DragValue::new(&mut a).speed(1.0).range(0.0..=1584.0).suffix(" pt")).changed() {
|
||
let _ = app.run("para.spacing", json!({"after": a}));
|
||
}
|
||
});
|
||
ui.end_row();
|
||
});
|
||
});
|
||
group(ui, "Arrange", None, app, |ui, app| {
|
||
big(ui, app, "position", "Position", "arrange.position", json!({}), false);
|
||
big(ui, app, "wrapText", "Wrap\nText", "arrange.wrap", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "bringForward", Some("Bring Forward"), "Bring Forward", "arrange.bringForward", json!({}), false);
|
||
small(ui, app, "sendBackward", Some("Send Backward"), "Send Backward", "arrange.sendBackward", json!({}), false);
|
||
small(ui, app, "selectionPane", Some("Selection Pane"), "Selection Pane", "arrange.selectionPane", json!({}), false);
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "align", None, "Align", "arrange.align", json!({}), false);
|
||
small(ui, app, "group", None, "Group", "arrange.group", json!({}), false);
|
||
small(ui, app, "rotate", None, "Rotate", "arrange.rotate", json!({}), false);
|
||
});
|
||
});
|
||
}
|
||
|
||
fn references(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Table of Contents", None, app, |ui, app| {
|
||
menu_button(ui, app, "toc", Some("Table of\nContents"), "Table of Contents", true, |ui, app| {
|
||
mi(ui, app, "Automatic Table 1 (3 levels)", "references.toc", json!({"levels": 3}));
|
||
mi(ui, app, "Automatic Table 2 (2 levels)", "references.toc", json!({"levels": 2, "title": "Table of Contents"}));
|
||
mi(ui, app, "Remove Table of Contents", "references.removeToc", json!({}));
|
||
});
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "addText", Some("Add Text"), "Add Text", false, |ui, app| {
|
||
mi(ui, app, "Do Not Show in TOC", "references.addText", json!({"level": 0}));
|
||
for l in 1..=3u64 {
|
||
mi(ui, app, &format!("Level {l}"), "references.addText", json!({"level": l}));
|
||
}
|
||
});
|
||
small(ui, app, "updateTable", Some("Update Table"), "Update Table", "references.updateToc", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Footnotes", None, app, |ui, app| {
|
||
big(ui, app, "footnote", "Insert\nFootnote", "references.footnote", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "endnote", Some("Insert Endnote"), "Insert Endnote", "references.endnote", json!({}), false);
|
||
small(ui, app, "nextFootnote", Some("Next Footnote"), "Next Footnote", "references.nextFootnote", json!({}), false);
|
||
small(ui, app, "showNotes", Some("Show Notes"), "Show Notes", "references.notes", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Research", None, app, |ui, app| {
|
||
big(ui, app, "researcher", "Researcher", "references.researcher", json!({}), false);
|
||
});
|
||
group(ui, "Citations & Bibliography", None, app, |ui, app| {
|
||
big(ui, app, "citation", "Insert\nCitation", "references.citation", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "sources", Some("Manage Sources"), "Manage Sources", "references.sources", json!({}), false);
|
||
small(ui, app, "styles", Some("Style: APA"), "Citation Style", "references.citationStyle", json!({}), false);
|
||
small(ui, app, "bibliography", Some("Bibliography"), "Bibliography", "references.bibliography", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Captions", None, app, |ui, app| {
|
||
big(ui, app, "caption", "Insert\nCaption", "references.caption", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(
|
||
ui,
|
||
app,
|
||
"tableOfFigures",
|
||
Some("Insert Table of Figures"),
|
||
"Insert Table of Figures",
|
||
"references.tableOfFigures",
|
||
json!({}),
|
||
false,
|
||
);
|
||
small(ui, app, "update", Some("Update Table"), "Update Table of Figures", "references.updateFigures", json!({}), false);
|
||
small(ui, app, "crossRef", Some("Cross-reference"), "Cross-reference", "insert.crossReference", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Index", None, app, |ui, app| {
|
||
big(ui, app, "markEntry", "Mark\nEntry", "references.markEntry", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "index", Some("Insert Index"), "Insert Index", "references.index", json!({}), false);
|
||
small(ui, app, "update", Some("Update Index"), "Update Index", "references.updateIndex", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Table of Authorities", None, app, |ui, app| {
|
||
big(ui, app, "markCitation", "Mark\nCitation", "references.markCitation", json!({}), false);
|
||
small(ui, app, "tableOfAuthorities", None, "Insert Table of Authorities", "references.tableOfAuthorities", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn mailings(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Create", None, app, |ui, app| {
|
||
big(ui, app, "envelope", "Envelopes", "mailings.envelopes", json!({}), false);
|
||
big(ui, app, "labels", "Labels", "mailings.labels", json!({}), false);
|
||
});
|
||
group(ui, "Start Mail Merge", None, app, |ui, app| {
|
||
big(ui, app, "mailMerge", "Start Mail\nMerge", "mailings.start", json!({}), false);
|
||
big(ui, app, "recipients", "Select\nRecipients", "mailings.recipients", json!({}), false);
|
||
big(ui, app, "editRecipients", "Edit\nRecipient List", "mailings.editRecipients", json!({}), false);
|
||
});
|
||
group(ui, "Write & Insert Fields", None, app, |ui, app| {
|
||
big(ui, app, "highlightFields", "Highlight\nMerge Fields", "mailings.highlightFields", json!({}), false);
|
||
big(ui, app, "addressBlock", "Address\nBlock", "mailings.addressBlock", json!({}), false);
|
||
big(ui, app, "greetingLine", "Greeting\nLine", "mailings.greetingLine", json!({}), false);
|
||
big(ui, app, "mergeField", "Insert Merge\nField", "mailings.insertField", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "rules", Some("Rules"), "Rules", "mailings.rules", json!({}), false);
|
||
small(ui, app, "matchFields", Some("Match Fields"), "Match Fields", "mailings.matchFields", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Preview Results", None, app, |ui, app| {
|
||
big(ui, app, "preview", "Preview\nResults", "mailings.preview", json!({}), false);
|
||
stack(ui, |ui| {
|
||
crate::widgets::row(ui, |ui| {
|
||
small(ui, app, "previous", None, "Previous Record", "mailings.previous", json!({}), false);
|
||
small(ui, app, "next", None, "Next Record", "mailings.next", json!({}), false);
|
||
});
|
||
small(ui, app, "find", Some("Find Recipient"), "Find Recipient", "mailings.findRecipient", json!({}), false);
|
||
small(ui, app, "checkErrors", Some("Check for Errors"), "Check for Errors", "mailings.checkErrors", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Finish", None, app, |ui, app| {
|
||
big(ui, app, "finish", "Finish &\nMerge", "mailings.finish", json!({}), false);
|
||
});
|
||
}
|
||
|
||
/// Zotero (`docs/zotero.md`): the Zotero desktop app does the work in its own window.
|
||
fn zotero(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Citations", None, app, |ui, app| {
|
||
big(ui, app, "citation", "Add/Edit\nCitation", "ui.zotero.addEditCitation", json!({}), false);
|
||
big(ui, app, "addNote", "Add\nNote", "ui.zotero.addNote", json!({}), false);
|
||
big(ui, app, "pastCitation", "Move Past\nCitation", "caret.pastCitation", json!({}), false);
|
||
});
|
||
group(ui, "Bibliography", None, app, |ui, app| {
|
||
big(ui, app, "bibliography", "Add/Edit\nBibliography", "ui.zotero.addEditBibliography", json!({}), false);
|
||
});
|
||
group(ui, "Document", None, app, |ui, app| {
|
||
big(ui, app, "update", "Refresh", "ui.zotero.refresh", json!({}), false);
|
||
big(ui, app, "docPrefs", "Document\nPreferences", "ui.zotero.setDocPrefs", json!({}), false);
|
||
big(ui, app, "unlinkCitations", "Unlink\nCitations", "ui.zotero.removeCodes", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn review(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Proofing", None, app, |ui, app| {
|
||
big(ui, app, "spelling", "Spelling &\nGrammar", "review.spelling", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "thesaurus", Some("Thesaurus"), "Thesaurus", "review.thesaurus", json!({}), false);
|
||
small(ui, app, "wordCount", Some("Word Count"), "Word Count", "ui.dialog", json!({"name": "wordCount"}), false);
|
||
});
|
||
});
|
||
group(ui, "Speech", None, app, |ui, app| {
|
||
big(ui, app, "readAloud", "Read\nAloud", "review.readAloud", json!({}), false);
|
||
});
|
||
group(ui, "Accessibility", None, app, |ui, app| {
|
||
big(ui, app, "accessibility", "Check\nAccessibility", "file.accessibility", json!({}), false);
|
||
});
|
||
group(ui, "Language", None, app, |ui, app| {
|
||
big(ui, app, "translate", "Translate", "review.translate", json!({}), false);
|
||
big(ui, app, "language", "Language", "review.language", json!({}), false);
|
||
});
|
||
group(ui, "Comments", None, app, |ui, app| {
|
||
big(ui, app, "newComment", "New\nComment", "review.newComment", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "deleteComment", Some("Delete"), "Delete Comment", "review.deleteComment", json!({}), false);
|
||
small(ui, app, "prevComment", Some("Previous"), "Previous Comment", "review.previousComment", json!({}), false);
|
||
small(ui, app, "nextComment", Some("Next"), "Next Comment", "review.nextComment", json!({}), false);
|
||
});
|
||
let shown = app.session.view.comments_pane;
|
||
big(ui, app, "showComments", "Show\nComments", "view.commentsPane", json!({"value": !shown}), false);
|
||
});
|
||
group(ui, "Tracking", None, app, |ui, app| {
|
||
let on = app.session.doc.settings.track_changes;
|
||
let r = big(ui, app, "trackChanges", if on { "Track\nChanges ✓" } else { "Track\nChanges" }, "review.trackChanges", json!({}), false);
|
||
let _ = r;
|
||
stack(ui, |ui| {
|
||
menu_button(
|
||
ui,
|
||
app,
|
||
"markup",
|
||
Some(if app.session.view.show_markup { "All Markup" } else { "No Markup" }),
|
||
"Display for Review",
|
||
false,
|
||
|ui, app| {
|
||
mi(ui, app, "All Markup", "review.markup", json!({"value": "all"}));
|
||
mi(ui, app, "No Markup", "review.markup", json!({"value": "noMarkup"}));
|
||
},
|
||
);
|
||
small(ui, app, "reviewingPane", Some("Reviewing Pane"), "Reviewing Pane", "review.changes", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Changes", None, app, |ui, app| {
|
||
menu_button(ui, app, "accept", Some("Accept"), "Accept", true, |ui, app| {
|
||
mi(ui, app, "Accept This Change", "review.accept", json!({}));
|
||
mi(ui, app, "Accept All Changes", "review.acceptAll", json!({}));
|
||
});
|
||
menu_button(ui, app, "reject", Some("Reject"), "Reject", true, |ui, app| {
|
||
mi(ui, app, "Reject Change", "review.reject", json!({}));
|
||
mi(ui, app, "Reject All Changes", "review.rejectAll", json!({}));
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "prevChange", Some("Previous"), "Previous Change", "review.previousChange", json!({}), false);
|
||
small(ui, app, "nextChange", Some("Next"), "Next Change", "review.nextChange", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Compare", None, app, |ui, app| {
|
||
big(ui, app, "compare", "Compare", "review.compare", json!({}), false);
|
||
});
|
||
group(ui, "Protect", None, app, |ui, app| {
|
||
big(ui, app, "blockAuthors", "Block\nAuthors", "review.blockAuthors", json!({}), false);
|
||
big(ui, app, "restrict", "Restrict\nEditing", "review.restrict", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn view(app: &mut WordApp, ui: &mut Ui) {
|
||
let v = app.session.view.clone();
|
||
group(ui, "Views", None, app, |ui, app| {
|
||
big(ui, app, "readMode", "Read\nMode", "view.readMode", json!({}), false);
|
||
big(ui, app, "printLayout", "Print\nLayout", "view.printLayout", json!({}), false);
|
||
big(ui, app, "webLayout", "Web\nLayout", "view.webLayout", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "outline", Some("Outline"), "Outline", "view.outline", json!({}), false);
|
||
small(ui, app, "draft", Some("Draft"), "Draft", "view.draft", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Immersive", None, app, |ui, app| {
|
||
big(ui, app, "focus", "Focus", "view.focus", json!({}), false);
|
||
big(ui, app, "immersive", "Immersive\nReader", "view.immersive", json!({}), false);
|
||
});
|
||
group(ui, "Page Movement", None, app, |ui, app| {
|
||
big(ui, app, "vertical", "Vertical", "view.vertical", json!({}), false);
|
||
big(ui, app, "sideToSide", "Side\nto Side", "view.sideToSide", json!({}), false);
|
||
});
|
||
group(ui, "Show", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
let mut r = v.ruler;
|
||
if ui.checkbox(&mut r, tl!("Ruler")).changed() {
|
||
let _ = app.run("view.ruler", json!({"value": r}));
|
||
}
|
||
let mut g = v.gridlines;
|
||
if ui.checkbox(&mut g, tl!("Gridlines")).changed() {
|
||
let _ = app.run("view.gridlines", json!({"value": g}));
|
||
}
|
||
let mut n = v.nav_pane;
|
||
if ui.checkbox(&mut n, tl!("Navigation Pane")).changed() {
|
||
let _ = app.run("view.navigationPane", json!({"value": n}));
|
||
}
|
||
});
|
||
});
|
||
group(ui, "Zoom", None, app, |ui, app| {
|
||
big(ui, app, "zoom", "Zoom", "ui.dialog", json!({"name": "zoom"}), false);
|
||
big(ui, app, "zoom100", "100%", "view.zoom100", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "onePage", Some("One Page"), "One Page", "view.onePage", json!({}), v.fit == "onePage");
|
||
small(ui, app, "multiplePages", Some("Multiple Pages"), "Multiple Pages", "view.multiplePages", json!({}), v.multi_page);
|
||
small(ui, app, "pageWidth", Some("Page Width"), "Page Width", "view.pageWidth", json!({}), v.fit == "pageWidth");
|
||
});
|
||
});
|
||
group(ui, "Dark Mode", None, app, |ui, app| {
|
||
big(ui, app, "darkMode", "Switch\nModes", "view.darkMode", json!({}), false);
|
||
});
|
||
group(ui, "Window", None, app, |ui, app| {
|
||
big(ui, app, "newWindow", "New\nWindow", "view.newWindow", json!({}), false);
|
||
big(ui, app, "arrangeAll", "Arrange\nAll", "view.arrangeAll", json!({}), false);
|
||
big(ui, app, "split", "Split", "view.split", json!({}), false);
|
||
});
|
||
group(ui, "Macros", None, app, |ui, app| {
|
||
big(ui, app, "macros", "Macros", "tools.macros", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn help(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Help", None, app, |ui, app| {
|
||
big(ui, app, "help", "Help", "ui.dialog", json!({"name": "about"}), false);
|
||
big(ui, app, "discord", "Community\nDiscord", "ui.discord", json!({}), false);
|
||
});
|
||
group(ui, "Agents", None, app, |ui, app| {
|
||
big(ui, app, "macros", "Commands", "ui.dialog", json!({"name": "commands"}), false);
|
||
});
|
||
}
|
||
|
||
fn picture_format(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Adjust", None, app, |ui, app| {
|
||
menu_button(ui, app, "picture", Some("Corrections"), "Brightness, contrast and sharpness", true, |ui, app| {
|
||
for (l, b, c) in
|
||
[("Brighter +20%", 20.0, 0.0), ("Darker −20%", -20.0, 0.0), ("More Contrast +20%", 0.0, 20.0), ("Less Contrast −20%", 0.0, -20.0)]
|
||
{
|
||
mi(ui, app, l, "picture.corrections", json!({"brightness": b, "contrast": c}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Sharpen", "picture.corrections", json!({"sharpen": 40.0}));
|
||
mi(ui, app, "Soften", "picture.corrections", json!({"sharpen": -40.0}));
|
||
});
|
||
menu_button(ui, app, "picture", Some("Color"), "Saturation and recolor", true, |ui, app| {
|
||
for (l, v) in [("Full Saturation 100%", 100.0), ("Muted 50%", 50.0), ("Gray 0%", 0.0), ("Vivid 200%", 200.0)] {
|
||
mi(ui, app, l, "picture.color", json!({"mode": "saturation", "saturation": v}));
|
||
}
|
||
ui.separator();
|
||
mi(ui, app, "Grayscale", "picture.color", json!({"mode": "grayscale"}));
|
||
mi(ui, app, "Sepia", "picture.color", json!({"mode": "sepia"}));
|
||
mi(ui, app, "Washout", "picture.color", json!({"mode": "washout"}));
|
||
});
|
||
menu_button(ui, app, "picture", Some("Transparency"), "Picture transparency", true, |ui, app| {
|
||
for v in [0.0, 15.0, 30.0, 50.0, 65.0, 80.0, 95.0] {
|
||
mi(ui, app, &format!("{v:.0}%"), "picture.transparency", json!({"percent": v}));
|
||
}
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "picture", Some("Change"), "Change Picture", "ui.changePicture", json!({}), false);
|
||
small(ui, app, "picture", Some("Reset"), "Reset Picture", "picture.reset", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Picture Styles", None, app, |ui, app| {
|
||
menu_button(ui, app, "picture", Some("Styles"), "Frames and effects", true, |ui, app| {
|
||
for (l, s) in [
|
||
("Simple Frame", "simpleFrame"),
|
||
("Thick Frame", "thickFrame"),
|
||
("Rounded", "rounded"),
|
||
("Soft Edge", "softEdge"),
|
||
("Shadow", "shadow"),
|
||
] {
|
||
mi(ui, app, l, "picture.style", json!({"style": s}));
|
||
}
|
||
});
|
||
menu_button(ui, app, "picture", Some("Border"), "Picture border", true, |ui, app| {
|
||
for (l, w) in [("Thin", 2), ("Medium", 4), ("Thick", 8)] {
|
||
mi(ui, app, l, "picture.border", json!({"width": w}));
|
||
}
|
||
});
|
||
});
|
||
group(ui, "Arrange", None, app, |ui, app| {
|
||
big(ui, app, "position", "Position", "arrange.position", json!({}), false);
|
||
big(ui, app, "wrapText", "Wrap\nText", "arrange.wrap", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "rotate", Some("Rotate"), "Rotate", "arrange.rotate", json!({}), false);
|
||
small(ui, app, "align", Some("Align"), "Align", "arrange.align", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Size", None, app, |ui, app| {
|
||
let (media, off, w0, h0, crop0, alt0) = match wordcraft_engine::cmd::objects::selected(&app.session) {
|
||
Some((pos, wordcraft_doc::para::InlineObject::Image { media, w, h, alt, crop, .. })) => (media, pos.off, w, h, crop, alt),
|
||
_ => (String::new(), 0, 0.0, 0.0, [0.0; 4], String::new()),
|
||
};
|
||
// `picture.size` locks the aspect ratio unless told otherwise, so one dimension suffices.
|
||
stack(ui, |ui| {
|
||
crate::widgets::row(ui, |ui| {
|
||
ui.label(egui::RichText::new(tl!("W:")).small());
|
||
let mut w = w0;
|
||
let r = ui.add(egui::DragValue::new(&mut w).speed(1.0).range(4.0..=2000.0).suffix(" pt"));
|
||
if r.changed() {
|
||
if r.dragged() && !r.drag_started() {
|
||
app.session.join_next_undo();
|
||
}
|
||
let _ = app.run("picture.size", json!({"width": w}));
|
||
}
|
||
ui.label(egui::RichText::new(tl!("H:")).small());
|
||
let mut h = h0;
|
||
let r = ui.add(egui::DragValue::new(&mut h).speed(1.0).range(4.0..=2000.0).suffix(" pt"));
|
||
if r.changed() {
|
||
if r.dragged() && !r.drag_started() {
|
||
app.session.join_next_undo();
|
||
}
|
||
let _ = app.run("picture.size", json!({"height": h}));
|
||
}
|
||
});
|
||
ui.add_space(2.0);
|
||
crate::widgets::row(ui, |ui| {
|
||
for (i, side) in ["L", "T", "R", "B"].iter().enumerate() {
|
||
ui.label(egui::RichText::new(tl!(side)).small());
|
||
let mut v = crop0[i] * 100.0;
|
||
let r = ui.add(egui::DragValue::new(&mut v).speed(0.5).range(0.0..=45.0).suffix("%"));
|
||
if r.changed() {
|
||
if r.dragged() && !r.drag_started() {
|
||
app.session.join_next_undo();
|
||
}
|
||
let mut c = crop0;
|
||
c[i] = (v / 100.0).clamp(0.0, 0.45);
|
||
let _ = app.run("picture.crop", json!({"left": c[0], "top": c[1], "right": c[2], "bottom": c[3]}));
|
||
}
|
||
}
|
||
if ui.button(egui::RichText::new(tl!("Reset")).small()).on_hover_text(tl!("Reset Crop")).clicked() {
|
||
let _ = app.run("picture.crop", json!({"left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0}));
|
||
}
|
||
});
|
||
ui.add_space(2.0);
|
||
crate::widgets::row(ui, |ui| {
|
||
ui.label(egui::RichText::new(tl!("Alt:")).small());
|
||
// Frame-local buffers lose keystrokes; keep the draft in egui temp memory keyed
|
||
// by picture (same pattern as the comment editor in panes.rs).
|
||
let key = egui::Id::new(("picture_alt", media.clone(), off));
|
||
let mut alt = ui.data(|d| d.get_temp::<String>(key)).unwrap_or_else(|| alt0.clone());
|
||
let r = ui.add(egui::TextEdit::singleline(&mut alt).desired_width(120.0).hint_text(tl!("Alt text")));
|
||
if r.changed() {
|
||
ui.data_mut(|d| d.insert_temp(key, alt.clone()));
|
||
}
|
||
if r.lost_focus() {
|
||
ui.data_mut(|d| d.remove::<String>(key));
|
||
// The click that took focus may have selected another picture; never commit
|
||
// one picture's draft onto another.
|
||
if alt != alt0 && app.selected_picture_media().as_deref() == Some(media.as_str()) {
|
||
let _ = app.run("picture.altText", json!({"text": alt}));
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
fn table_design(app: &mut WordApp, ui: &mut Ui) {
|
||
let look = app.session.sel.focus.path.cell().and_then(|(tp, _, _)| app.session.doc.table(app.session.sel.focus.story, &tp).map(|t| t.props.look));
|
||
group(ui, "Table Style Options", None, app, |ui, app| {
|
||
let Some(l) = look else { return };
|
||
egui::Grid::new("look").show(ui, |ui| {
|
||
for (row, items) in [
|
||
[("Header Row", "headerRow", l.header_row), ("First Column", "firstColumn", l.first_column)],
|
||
[("Total Row", "totalRow", l.total_row), ("Last Column", "lastColumn", l.last_column)],
|
||
[("Banded Rows", "bandedRows", l.banded_rows), ("Banded Columns", "bandedColumns", l.banded_columns)],
|
||
]
|
||
.iter()
|
||
.enumerate()
|
||
{
|
||
let _ = row;
|
||
for (label, key, val) in items {
|
||
let mut v = *val;
|
||
if ui.checkbox(&mut v, *label).changed() {
|
||
let _ = app.run("table.look", json!({ *key: v }));
|
||
}
|
||
}
|
||
ui.end_row();
|
||
}
|
||
});
|
||
});
|
||
group(ui, "Table Styles", None, app, |ui, app| {
|
||
let styles: Vec<(String, String)> = app
|
||
.session
|
||
.doc
|
||
.styles
|
||
.styles
|
||
.iter()
|
||
.filter(|s| s.kind == wordcraft_doc::StyleKind::Table && !s.hidden)
|
||
.map(|s| (s.id.clone(), s.name.clone()))
|
||
.collect();
|
||
egui::ScrollArea::horizontal().max_width(420.0).show(ui, |ui| {
|
||
ui.horizontal(|ui| {
|
||
for (id, name) in styles {
|
||
if crate::previews::table_style_tile(ui, app, &id).on_hover_text(name).clicked() {
|
||
let _ = app.run("table.style", json!({"style": id}));
|
||
}
|
||
}
|
||
});
|
||
});
|
||
stack(ui, |ui| {
|
||
split(ui, app, "shading", "Shading", "table.shading", json!({"color": app.canvas.last_shading.clone()}), false, None, |ui, app| {
|
||
mi(ui, app, "No Color", "table.shading", json!({"color": null}));
|
||
let theme = app.session.doc.settings.theme_colors.clone();
|
||
if let Some(hex) = color_grid(ui, &theme) {
|
||
app.canvas.last_shading = hex.clone();
|
||
let _ = app.run("table.shading", json!({"color": hex}));
|
||
ui.close();
|
||
}
|
||
});
|
||
});
|
||
});
|
||
group(ui, "Borders", None, app, |ui, app| {
|
||
menu_button(ui, app, "borders", Some("Borders"), "Borders", true, |ui, app| {
|
||
for (l, k) in [
|
||
("All Borders", "all"),
|
||
("Outside Borders", "outside"),
|
||
("Inside Borders", "inside"),
|
||
("No Border", "none"),
|
||
("Top Border", "top"),
|
||
("Bottom Border", "bottom"),
|
||
] {
|
||
mi(ui, app, l, "table.borders", json!({"kind": k}));
|
||
}
|
||
});
|
||
big(ui, app, "borderPainter", "Border\nPainter", "table.borderPainter", json!({}), false);
|
||
});
|
||
}
|
||
|
||
fn table_layout(app: &mut WordApp, ui: &mut Ui) {
|
||
group(ui, "Table", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
menu_button(ui, app, "selectTable", Some("Select"), "Select", false, |ui, app| {
|
||
mi(ui, app, "Select Cell", "table.selectCell", json!({}));
|
||
mi(ui, app, "Select Row", "table.selectRow", json!({}));
|
||
mi(ui, app, "Select Table", "table.selectTable", json!({}));
|
||
});
|
||
let g = app.session.view.gridlines;
|
||
small(ui, app, "gridlines", Some("View Gridlines"), "View Gridlines", "view.gridlines", json!({}), g);
|
||
small(ui, app, "properties", Some("Properties"), "Table Properties", "table.properties", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Rows & Columns", None, app, |ui, app| {
|
||
menu_button(ui, app, "deleteTable", Some("Delete"), "Delete", true, |ui, app| {
|
||
mi(ui, app, "Delete Cells", "table.deleteCells", json!({}));
|
||
mi(ui, app, "Delete Columns", "table.deleteColumn", json!({}));
|
||
mi(ui, app, "Delete Rows", "table.deleteRow", json!({}));
|
||
mi(ui, app, "Delete Table", "table.deleteTable", json!({}));
|
||
});
|
||
big(ui, app, "insertAbove", "Insert\nAbove", "table.insertRowAbove", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "insertBelow", Some("Insert Below"), "Insert Below", "table.insertRowBelow", json!({}), false);
|
||
small(ui, app, "insertLeft", Some("Insert Left"), "Insert Left", "table.insertColumnLeft", json!({}), false);
|
||
small(ui, app, "insertRight", Some("Insert Right"), "Insert Right", "table.insertColumnRight", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Merge", None, app, |ui, app| {
|
||
stack(ui, |ui| {
|
||
small(ui, app, "merge", Some("Merge Cells"), "Merge Cells", "table.merge", json!({}), false);
|
||
small(ui, app, "splitCells", Some("Split Cells"), "Split Cells", "table.split", json!({}), false);
|
||
small(ui, app, "splitTable", Some("Split Table"), "Split Table", "table.splitTable", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Cell Size", None, app, |ui, app| {
|
||
menu_button(ui, app, "autofit", Some("AutoFit"), "AutoFit", true, |ui, app| {
|
||
mi(ui, app, "AutoFit Contents", "table.autofit", json!({"mode": "contents"}));
|
||
mi(ui, app, "AutoFit Window", "table.autofit", json!({"mode": "window"}));
|
||
mi(ui, app, "Fixed Column Width", "table.autofit", json!({"mode": "fixed"}));
|
||
});
|
||
stack(ui, |ui| {
|
||
small(ui, app, "distributeRows", Some("Distribute Rows"), "Distribute Rows", "table.distributeRows", json!({}), false);
|
||
small(ui, app, "distributeCols", Some("Distribute Columns"), "Distribute Columns", "table.distributeColumns", json!({}), false);
|
||
});
|
||
});
|
||
group(ui, "Alignment", None, app, |ui, app| {
|
||
egui::Grid::new("cellalign").spacing(vec2(1.0, 1.0)).show(ui, |ui| {
|
||
for row in [["topLeft", "topCenter", "topRight"], ["centerLeft", "center", "centerRight"], ["bottomLeft", "bottomCenter", "bottomRight"]]
|
||
{
|
||
for v in row {
|
||
small(ui, app, "cellAlign", None, v, "table.cellAlign", json!({"value": v}), false);
|
||
}
|
||
ui.end_row();
|
||
}
|
||
});
|
||
big(ui, app, "textDirection", "Text\nDirection", "table.textDirection", json!({}), false);
|
||
big(ui, app, "cellMargins", "Cell\nMargins", "table.cellMargins", json!({}), false);
|
||
});
|
||
group(ui, "Data", None, app, |ui, app| {
|
||
big(ui, app, "sort", "Sort", "table.sort", json!({}), false);
|
||
stack(ui, |ui| {
|
||
small(ui, app, "repeatHeader", Some("Repeat Header Rows"), "Repeat Header Rows", "table.repeatHeader", json!({}), false);
|
||
small(ui, app, "toText", Some("Convert to Text"), "Convert to Text", "table.toText", json!({}), false);
|
||
small(ui, app, "formula", Some("Formula"), "Formula", "table.formula", json!({}), false);
|
||
});
|
||
});
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use wordcraft_engine::Session;
|
||
|
||
fn png_data() -> String {
|
||
let img = image::RgbaImage::from_fn(20, 10, |x, y| {
|
||
if x > 2 && x < 17 && y > 2 && y < 7 { image::Rgba([200, 30, 30, 255]) } else { image::Rgba([255, 255, 255, 255]) }
|
||
});
|
||
let mut b = Vec::new();
|
||
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut b), image::ImageFormat::Png).unwrap();
|
||
wordcraft_engine::cmd::insert::base64_encode(&b)
|
||
}
|
||
|
||
#[test]
|
||
fn picture_format_tab_appears_when_picture_selected() {
|
||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||
assert!(!has_picture_selected(&s));
|
||
assert!(!contextual_tabs(&s).contains(&"Picture Format"));
|
||
s.run("insert.picture", &json!({"data": png_data()})).unwrap();
|
||
assert!(has_picture_selected(&s));
|
||
assert!(contextual_tabs(&s).contains(&"Picture Format"));
|
||
s.run("select.collapse", &json!({"end": true})).unwrap();
|
||
s.run("text.insert", &json!({"text": "x"})).unwrap();
|
||
assert!(!has_picture_selected(&s));
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_tab_falls_back_when_contextual_tab_expires() {
|
||
let all = ["Home", "Table Design", "Picture Format"];
|
||
assert_eq!(resolve_tab("Picture Format", &all), "Picture Format");
|
||
assert_eq!(resolve_tab("Picture Format", &["Home"]), "Home");
|
||
assert_eq!(resolve_tab("Table Design", &["Home"]), "Home");
|
||
assert_eq!(resolve_tab("Home", &["Home"]), "Home");
|
||
}
|
||
}
|