mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-10 20:29:51 -06:00
285 lines
12 KiB
Rust
285 lines
12 KiB
Rust
use super::common::{BUILT_IN_LABELS, LEADERS, choice, heading, leader_char, sample};
|
||
use super::index::body_field;
|
||
use super::shell::{Next, go};
|
||
use super::{Dialog, FieldCode, RefsDialog, Serialize, StyleKind, Ui, Value, WordApp, buttons, json, vec2};
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Table of Contents
|
||
|
||
/// The Custom Table of Contents dialog's fields (with its Options).
|
||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct TocForm {
|
||
pub page_numbers: bool,
|
||
pub right_align: bool,
|
||
pub leader: String,
|
||
pub hyperlinks: bool,
|
||
pub levels: u8,
|
||
/// Options: heading styles (`\o`), outline levels (`\u`), TC fields (`\f`).
|
||
pub heading_styles: bool,
|
||
pub outline_levels: bool,
|
||
pub tc_fields: bool,
|
||
/// Options: other paragraph styles with a TOC level (0 = not in the table), by style name.
|
||
pub style_levels: Vec<(String, u8)>,
|
||
}
|
||
|
||
impl TocForm {
|
||
/// The table in the document, or the defaults for a new one.
|
||
pub fn read(app: &WordApp) -> TocForm {
|
||
let f = body_field(app, "TOC").unwrap_or_else(|| FieldCode::parse(r#"TOC \o "1-3" \h \z \u"#));
|
||
let levels = f.arg('o').and_then(wordcraft_engine::cmd::references::level_range).map_or(3, |r| r.1);
|
||
let listed: Vec<(String, u8)> = f
|
||
.arg('t')
|
||
.map(|t| {
|
||
let parts: Vec<&str> = t.split(',').map(str::trim).collect();
|
||
parts.chunks(2).filter_map(|c| Some((c.first()?.to_string(), c.get(1)?.parse::<u8>().ok()?.clamp(1, 9)))).collect()
|
||
})
|
||
.unwrap_or_default();
|
||
let style_levels = app
|
||
.session
|
||
.doc
|
||
.styles
|
||
.styles
|
||
.iter()
|
||
.filter(|s| s.kind == StyleKind::Paragraph && !s.hidden && !s.id.starts_with("Heading") && !s.id.starts_with("TOC"))
|
||
.map(|s| (s.name.clone(), listed.iter().find(|(n, _)| n.eq_ignore_ascii_case(&s.name)).map_or(0, |(_, l)| *l)))
|
||
.collect();
|
||
// Right-aligned page numbers: no `\p` separator, or a tab one.
|
||
let leader = app
|
||
.session
|
||
.doc
|
||
.body
|
||
.iter()
|
||
.filter_map(|b| b.as_para())
|
||
.find(|p| p.props.style.as_deref().is_some_and(|s| s.starts_with("TOC") && s != "TOCHeading"))
|
||
.and_then(|p| p.props.tabs.as_ref()?.last().map(|t| t.leader));
|
||
TocForm {
|
||
page_numbers: !f.has('n'),
|
||
right_align: f.arg('p').is_none_or(|p| p.contains('\t')),
|
||
leader: match leader {
|
||
Some(wordcraft_doc::props::TabLeader::None) => "none",
|
||
Some(wordcraft_doc::props::TabLeader::Hyphen) => "hyphen",
|
||
Some(wordcraft_doc::props::TabLeader::Underscore) => "underscore",
|
||
_ => "dot",
|
||
}
|
||
.into(),
|
||
hyperlinks: f.has('h'),
|
||
levels: levels.clamp(1, 9),
|
||
heading_styles: f.has('o'),
|
||
outline_levels: f.has('u'),
|
||
tc_fields: f.has('f'),
|
||
style_levels,
|
||
}
|
||
}
|
||
|
||
/// `references.toc` parameters.
|
||
pub fn params(&self) -> Value {
|
||
let styles: serde_json::Map<String, Value> = self.style_levels.iter().filter(|(_, l)| *l > 0).map(|(n, l)| (n.clone(), json!(l))).collect();
|
||
json!({
|
||
"levels": self.levels,
|
||
"pageNumbers": self.page_numbers,
|
||
"rightAlign": self.right_align,
|
||
"tabLeader": self.leader,
|
||
"hyperlinks": self.hyperlinks,
|
||
"headingStyles": self.heading_styles,
|
||
"outlineLevels": self.outline_levels,
|
||
"tcFields": self.tc_fields,
|
||
"styleLevels": styles,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Draws the TOC preview and page-number, right-alignment, tab-leader, hyperlink and level
|
||
/// controls, with Options and Modify jumps and `references.toc` on OK.
|
||
pub(super) fn toc_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TocForm) -> Next {
|
||
let lines: Vec<(usize, String, String)> = (1..=f.levels.min(3))
|
||
.map(|l| {
|
||
let name = crate::i18n::fmt(tl!("Heading {n}"), &[("n", &l.to_string())]);
|
||
(l as usize - 1, name, if f.page_numbers { (l * 2 - 1).to_string() } else { String::new() })
|
||
})
|
||
.collect();
|
||
sample(ui, &lines, f.right_align, leader_char(&f.leader));
|
||
ui.add_space(6.0);
|
||
ui.checkbox(&mut f.page_numbers, tl!("Include page numbers"));
|
||
ui.add_enabled_ui(f.page_numbers, |ui| {
|
||
ui.checkbox(&mut f.right_align, tl!("Page numbers at the right margin"));
|
||
ui.horizontal(|ui| {
|
||
let leader_label = ui.label(tl!("Tab leader:"));
|
||
ui.add_enabled_ui(f.right_align, |ui| {
|
||
let opts: Vec<(String, &str)> = LEADERS.iter().map(|(v, l)| (v.to_string(), *l)).collect();
|
||
choice(ui, "toc_leader", &mut f.leader, &opts, &leader_label);
|
||
});
|
||
});
|
||
});
|
||
ui.checkbox(&mut f.hyperlinks, tl!("Link entries to their headings"));
|
||
ui.horizontal(|ui| {
|
||
let levels_label = ui.label(tl!("Levels:"));
|
||
ui.add(egui::DragValue::new(&mut f.levels).range(1..=9)).labelled_by(levels_label.id);
|
||
});
|
||
ui.add_space(4.0);
|
||
let mut then = None;
|
||
ui.horizontal(|ui| {
|
||
if ui.button(tl!("Options…")).clicked() {
|
||
then = Some(RefsDialog::TocOptions { was: Box::new(f.clone()), form: f.clone() });
|
||
}
|
||
if ui.button(tl!("Modify…")).clicked() {
|
||
then = Some(RefsDialog::TocStyles { form: f.clone(), level: 1 });
|
||
}
|
||
});
|
||
if let Some(g) = then {
|
||
return go(g);
|
||
}
|
||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||
if ok && let Err(e) = app.run("references.toc", f.params()) {
|
||
app.status(e);
|
||
}
|
||
if ok || cancel { Next::Close } else { Next::Stay }
|
||
}
|
||
|
||
/// Draws the TOC sources: heading styles, per-style TOC levels, outline levels and TC entry
|
||
/// fields, with Reset. OK returns the edited form and Cancel the original.
|
||
pub(super) fn toc_options_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TocForm, was: &TocForm) -> Next {
|
||
heading(ui, "Collect entries from:");
|
||
ui.checkbox(&mut f.heading_styles, crate::i18n::fmt(tl!("Heading styles (levels 1–{n})"), &[("n", &f.levels.to_string())]));
|
||
ui.label(tl!("Other styles and their TOC level:"));
|
||
// A solid scroll bar takes its own room beside the list instead of floating over the level boxes.
|
||
ui.spacing_mut().scroll = egui::style::ScrollStyle::solid();
|
||
egui::ScrollArea::vertical().max_height(180.0).show(ui, |ui| {
|
||
egui::Grid::new("toc_styles").num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
|
||
for (name, level) in f.style_levels.iter_mut() {
|
||
// Built-in styles read in the interface language; the document keeps the English name.
|
||
let shown = app.session.doc.styles.styles.iter().find(|st| st.name == *name).map_or_else(|| name.clone(), crate::i18n::style_name);
|
||
let style_label = ui.label(shown);
|
||
ui.add(egui::DragValue::new(level).range(0..=9).custom_formatter(|v, _| if v < 0.5 { String::new() } else { format!("{v:.0}") }))
|
||
.labelled_by(style_label.id);
|
||
ui.end_row();
|
||
}
|
||
});
|
||
});
|
||
ui.checkbox(&mut f.outline_levels, tl!("Outline levels"));
|
||
ui.checkbox(&mut f.tc_fields, tl!("TC entry fields"));
|
||
ui.add_space(4.0);
|
||
if ui.button(tl!("Reset")).clicked() {
|
||
f.heading_styles = true;
|
||
f.outline_levels = true;
|
||
f.tc_fields = false;
|
||
for (_, l) in f.style_levels.iter_mut() {
|
||
*l = 0;
|
||
}
|
||
}
|
||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||
if ok {
|
||
return go(RefsDialog::Toc(f.clone()));
|
||
}
|
||
if cancel {
|
||
return go(RefsDialog::Toc(was.clone()));
|
||
}
|
||
Next::Stay
|
||
}
|
||
|
||
/// Modify… in Table of Contents: pick a TOC level style and edit it in Modify Style, which
|
||
/// comes back here when it closes.
|
||
pub(super) fn toc_styles_ui(app: &mut WordApp, ui: &mut Ui, f: &mut TocForm, level: &mut u8) -> Next {
|
||
ui.label(tl!("Styles:"));
|
||
egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
||
for l in 1..=9u8 {
|
||
let name = app.session.doc.styles.get(&format!("TOC{l}")).map(|s| s.name.clone()).unwrap_or_else(|| format!("TOC {l}"));
|
||
ui.selectable_value(level, l, name);
|
||
}
|
||
});
|
||
ui.add_space(6.0);
|
||
if ui.button(tl!("Modify…")).clicked()
|
||
&& let Some(mut d) = Dialog::modify_style(app, &format!("TOC{level}"))
|
||
{
|
||
if let Dialog::ModifyStyle { resume, .. } = &mut d {
|
||
*resume = Some(Box::new(Dialog::Refs(Box::new(RefsDialog::TocStyles { form: f.clone(), level: *level }))));
|
||
}
|
||
app.dialog = Some(d);
|
||
return Next::Close;
|
||
}
|
||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||
if ok || cancel { go(RefsDialog::Toc(f.clone())) } else { Next::Stay }
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Table of Figures
|
||
|
||
/// The Table of Figures dialog's fields (#3013).
|
||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct FiguresForm {
|
||
/// The caption label whose captions are listed.
|
||
pub label: String,
|
||
/// Built-in labels, the user's own and any the document's captions use.
|
||
pub labels: Vec<String>,
|
||
pub include_label: bool,
|
||
pub page_numbers: bool,
|
||
pub right_align: bool,
|
||
pub leader: String,
|
||
}
|
||
|
||
impl FiguresForm {
|
||
pub fn read(app: &WordApp) -> FiguresForm {
|
||
let mut labels: Vec<String> = BUILT_IN_LABELS.iter().map(ToString::to_string).collect();
|
||
for l in app.ui.caption_labels.iter().cloned().chain(wordcraft_engine::cmd::citations::caption_labels(&app.session)) {
|
||
if !labels.contains(&l) {
|
||
labels.push(l);
|
||
}
|
||
}
|
||
FiguresForm { label: "Figure".into(), labels, include_label: true, page_numbers: true, right_align: true, leader: "dot".into() }
|
||
}
|
||
|
||
/// `references.tableOfFigures` parameters.
|
||
pub fn params(&self) -> Value {
|
||
json!({
|
||
"label": self.label,
|
||
"includeLabel": self.include_label,
|
||
"pageNumbers": self.page_numbers,
|
||
"rightAlign": self.right_align,
|
||
"tabLeader": self.leader,
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Draws the table-of-figures preview and label, include-label, page-number, right-alignment and
|
||
/// tab-leader controls, running `references.tableOfFigures` on OK.
|
||
pub(super) fn figures_ui(app: &mut WordApp, ui: &mut Ui, f: &mut FiguresForm) -> Next {
|
||
let lines: Vec<(usize, String, String)> = [(1, "1"), (2, "3")]
|
||
.iter()
|
||
.map(|(n, page)| {
|
||
let text = if f.include_label { format!("{} {n}: {}", tl!(&f.label), tl!("Text")) } else { tl!("Text").to_string() };
|
||
(0, text, if f.page_numbers { page.to_string() } else { String::new() })
|
||
})
|
||
.collect();
|
||
sample(ui, &lines, f.right_align, leader_char(&f.leader));
|
||
ui.add_space(6.0);
|
||
ui.horizontal(|ui| {
|
||
let label_label = ui.label(tl!("Label:"));
|
||
let labels: Vec<(String, &str)> = f.labels.iter().map(|l| (l.clone(), l.as_str())).collect();
|
||
let combo = craft_ui::ComboBox::from_id_salt("tof_label").selected_text(tl!(&f.label)).width(150.0).show_ui(ui, |ui| {
|
||
for (v, l) in &labels {
|
||
ui.selectable_value(&mut f.label, v.clone(), tl!(l));
|
||
}
|
||
});
|
||
combo.response.labelled_by(label_label.id);
|
||
});
|
||
ui.checkbox(&mut f.include_label, tl!("Include label and number"));
|
||
ui.checkbox(&mut f.page_numbers, tl!("Include page numbers"));
|
||
ui.add_enabled_ui(f.page_numbers, |ui| {
|
||
ui.checkbox(&mut f.right_align, tl!("Page numbers at the right margin"));
|
||
ui.horizontal(|ui| {
|
||
let leader_label = ui.label(tl!("Tab leader:"));
|
||
ui.add_enabled_ui(f.right_align, |ui| {
|
||
let opts: Vec<(String, &str)> = LEADERS.iter().map(|(v, l)| (v.to_string(), *l)).collect();
|
||
choice(ui, "tof_leader", &mut f.leader, &opts, &leader_label);
|
||
});
|
||
});
|
||
});
|
||
ui.add_space(4.0);
|
||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||
if ok && let Err(e) = app.run("references.tableOfFigures", f.params()) {
|
||
app.status(e);
|
||
}
|
||
if ok || cancel { Next::Close } else { Next::Stay }
|
||
}
|