New Table Style and Modify Table Style (Table Design > Table Styles) make real table styles in the document's style sheet: name, based on, and formatting for the whole table, the header row and banded rows (borders, fill, bold, font colour). The table at the caret takes the new style, it shows in the gallery, and other tools that read table styles by name (pandoc's custom-style, for one) see it. - engine: table.newStyle / table.modifyStyle with documented params (regions take borders, fill, bold, italic, colour, size, font; bandSize; para); table.style only accepts table styles now. - doc/layout: table styles gain header-row and row-band cell borders, row-band character formatting and rows per band, applied by layout. - docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are read and written, so custom styles round-trip. - ui: a compact dialog from the Styles menu next to the gallery; table style tiles redraw when a style changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
19e2e37ca6
commit
d8f71959d3
|
|
@ -9,7 +9,7 @@ and agent control. This file tracks where we are honestly. Generated numbers com
|
|||
| Measure | Value |
|
||||
|---|---|
|
||||
| Commands (every action, scriptable by CLI/MCP/control channel) | **422** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **378 / 428 (88%)** |
|
||||
| Feature catalog coverage (Word ribbon/menu features with a command) | **380 / 430 (88%)** |
|
||||
| **Estimated real feature parity** (depth and fidelity, not just a command) | **~62%** |
|
||||
| **Distance to alpha** | **~85% of the way; ≈20–25 h of Claude wall-clock work** |
|
||||
| **Distance to 100% parity** | **≈120–150 h of Claude Opus 5.5 wall-clock work** (with parallel agents) |
|
||||
|
|
|
|||
|
|
@ -52,7 +52,16 @@ pub struct TableStyleParts {
|
|||
pub cell_margins: Option<[f32; 4]>,
|
||||
pub header_fill: Option<Rgb>,
|
||||
pub header_chr: CharProps,
|
||||
/// Cell borders of the header row (`firstRow` region's `w:tcBorders`): `between` is the
|
||||
/// border between header rows, `inside_v` between its cells.
|
||||
pub header_borders: Option<Borders>,
|
||||
pub band_fill: Option<Rgb>,
|
||||
/// Character formatting of the odd row bands (`band1Horz` region).
|
||||
pub band_chr: CharProps,
|
||||
/// Cell borders of the odd row bands (`band1Horz` region's `w:tcBorders`).
|
||||
pub band_borders: Option<Borders>,
|
||||
/// Rows per band (`w:tblStyleRowBandSize`); one when unset.
|
||||
pub band_size: Option<u32>,
|
||||
pub first_col_chr: CharProps,
|
||||
pub total_chr: CharProps,
|
||||
pub total_border_top: Option<Border>,
|
||||
|
|
@ -62,17 +71,18 @@ impl TableStyleParts {
|
|||
/// Apply `patch` (a style further down a based-on chain) over these parts: what it sets wins,
|
||||
/// the rest is inherited; borders merge edge by edge.
|
||||
pub fn overlay(&mut self, patch: &TableStyleParts) {
|
||||
self.borders = match (self.borders, patch.borders) {
|
||||
(Some(b), Some(p)) => Some(Borders {
|
||||
top: p.top.or(b.top),
|
||||
left: p.left.or(b.left),
|
||||
bottom: p.bottom.or(b.bottom),
|
||||
right: p.right.or(b.right),
|
||||
between: p.between.or(b.between),
|
||||
inside_v: p.inside_v.or(b.inside_v),
|
||||
}),
|
||||
let merge = |b: Option<Borders>, p: Option<Borders>| match (b, p) {
|
||||
(Some(mut b), Some(p)) => {
|
||||
b.overlay(&p);
|
||||
Some(b)
|
||||
}
|
||||
(b, p) => p.or(b),
|
||||
};
|
||||
self.borders = merge(self.borders, patch.borders);
|
||||
self.header_borders = merge(self.header_borders, patch.header_borders);
|
||||
self.band_borders = merge(self.band_borders, patch.band_borders);
|
||||
self.band_size = patch.band_size.or(self.band_size);
|
||||
self.band_chr.overlay(&patch.band_chr);
|
||||
self.fill = patch.fill.or(self.fill);
|
||||
self.cell_margins = patch.cell_margins.or(self.cell_margins);
|
||||
self.header_fill = patch.header_fill.or(self.header_fill);
|
||||
|
|
|
|||
|
|
@ -444,9 +444,11 @@ impl Reader<'_> {
|
|||
}
|
||||
t.fill = tp.child("w:shd").and_then(props::shd_fill);
|
||||
t.cell_margins = tp.child("w:tblCellMar").map(props::margins);
|
||||
t.band_size = tp.child_val("w:tblStyleRowBandSize").and_then(u32_of).filter(|n| *n > 0).map(|n| n.min(1000));
|
||||
}
|
||||
for c in s.children("w:tblStylePr") {
|
||||
for c in s.children("w:tblStylePr").take(64) {
|
||||
let fill = c.child("w:tcPr").and_then(|p| p.child("w:shd")).and_then(props::shd_fill);
|
||||
let cell_borders = c.child("w:tcPr").and_then(|p| p.child("w:tcBorders")).map(props::borders);
|
||||
let chr = c.child("w:rPr").map(|r| self.pc.rpr(r)).unwrap_or_default();
|
||||
match c.attr("w:type") {
|
||||
Some("wholeTable") => {
|
||||
|
|
@ -464,8 +466,13 @@ impl Reader<'_> {
|
|||
Some("firstRow") => {
|
||||
t.header_fill = fill;
|
||||
t.header_chr = chr;
|
||||
t.header_borders = cell_borders;
|
||||
}
|
||||
Some("band1Horz") => {
|
||||
t.band_fill = fill;
|
||||
t.band_chr = chr;
|
||||
t.band_borders = cell_borders;
|
||||
}
|
||||
Some("band1Horz") => t.band_fill = fill,
|
||||
Some("firstCol") => t.first_col_chr = chr,
|
||||
Some("lastRow") => {
|
||||
t.total_chr = chr;
|
||||
|
|
|
|||
|
|
@ -622,8 +622,11 @@ fn style_xml(w: &mut W, st: &Style) {
|
|||
w.close("w:rPr");
|
||||
}
|
||||
if let Some(t) = &st.table {
|
||||
if t.borders.is_some() || t.cell_margins.is_some() {
|
||||
if t.borders.is_some() || t.cell_margins.is_some() || t.band_size.is_some() {
|
||||
w.open("w:tblPr", &[]);
|
||||
if let Some(n) = t.band_size {
|
||||
w.val("w:tblStyleRowBandSize", &n.clamp(1, 1000).to_string());
|
||||
}
|
||||
if let Some(b) = &t.borders {
|
||||
props::borders(w, "w:tblBorders", b, Some("w:insideH"), &[]);
|
||||
}
|
||||
|
|
@ -632,35 +635,40 @@ fn style_xml(w: &mut W, st: &Style) {
|
|||
}
|
||||
w.close("w:tblPr");
|
||||
}
|
||||
let cond =
|
||||
|w: &mut W, ty: &str, chr: &wordcraft_doc::CharProps, fill: Option<wordcraft_doc::Rgb>, top: Option<&wordcraft_doc::props::Border>| {
|
||||
if !props::has_rpr(chr) && fill.is_none() && top.is_none() {
|
||||
return;
|
||||
// One conditional formatting region (ECMA-376 §17.7.6): run properties, then cell borders
|
||||
// and shading.
|
||||
let cond = |w: &mut W,
|
||||
ty: &str,
|
||||
chr: &wordcraft_doc::CharProps,
|
||||
fill: Option<wordcraft_doc::Rgb>,
|
||||
borders: Option<wordcraft_doc::props::Borders>| {
|
||||
if !props::has_rpr(chr) && fill.is_none() && borders.is_none() {
|
||||
return;
|
||||
}
|
||||
w.open("w:tblStylePr", &[("w:type", ty)]);
|
||||
if props::has_rpr(chr) {
|
||||
w.open("w:rPr", &[]);
|
||||
props::rpr_inner(w, chr);
|
||||
w.close("w:rPr");
|
||||
}
|
||||
if fill.is_some() || borders.is_some() {
|
||||
w.open("w:tcPr", &[]);
|
||||
if let Some(bs) = &borders {
|
||||
props::borders(w, "w:tcBorders", bs, Some("w:insideH"), &[]);
|
||||
}
|
||||
w.open("w:tblStylePr", &[("w:type", ty)]);
|
||||
if props::has_rpr(chr) {
|
||||
w.open("w:rPr", &[]);
|
||||
props::rpr_inner(w, chr);
|
||||
w.close("w:rPr");
|
||||
if let Some(f) = fill {
|
||||
w.empty("w:shd", &[("w:val", "clear"), ("w:color", "auto"), ("w:fill", &f.hex())]);
|
||||
}
|
||||
if fill.is_some() || top.is_some() {
|
||||
w.open("w:tcPr", &[]);
|
||||
if let Some(b) = top {
|
||||
let bs = wordcraft_doc::props::Borders { top: Some(*b), ..Default::default() };
|
||||
props::borders(w, "w:tcBorders", &bs, None, &[]);
|
||||
}
|
||||
if let Some(f) = fill {
|
||||
w.empty("w:shd", &[("w:val", "clear"), ("w:color", "auto"), ("w:fill", &f.hex())]);
|
||||
}
|
||||
w.close("w:tcPr");
|
||||
}
|
||||
w.close("w:tblStylePr");
|
||||
};
|
||||
w.close("w:tcPr");
|
||||
}
|
||||
w.close("w:tblStylePr");
|
||||
};
|
||||
cond(w, "wholeTable", &wordcraft_doc::CharProps::default(), t.fill, None);
|
||||
cond(w, "firstRow", &t.header_chr, t.header_fill, None);
|
||||
cond(w, "lastRow", &t.total_chr, None, t.total_border_top.as_ref());
|
||||
cond(w, "firstRow", &t.header_chr, t.header_fill, t.header_borders);
|
||||
let total_top = t.total_border_top.map(|b| wordcraft_doc::props::Borders { top: Some(b), ..Default::default() });
|
||||
cond(w, "lastRow", &t.total_chr, None, total_top);
|
||||
cond(w, "firstCol", &t.first_col_chr, None, None);
|
||||
cond(w, "band1Horz", &wordcraft_doc::CharProps::default(), t.band_fill, None);
|
||||
cond(w, "band1Horz", &t.band_chr, t.band_fill, t.band_borders);
|
||||
}
|
||||
w.close("w:style");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -429,7 +429,8 @@ fn comments_round_trip() {
|
|||
}
|
||||
}
|
||||
|
||||
/// A table style's cell text formatting, whole-table shading and cell margins survive a save.
|
||||
/// A table style's cell text formatting, whole-table shading, cell margins and its header row and
|
||||
/// row band conditional formats (#146) survive a save.
|
||||
#[test]
|
||||
fn table_style_formatting_round_trips() {
|
||||
use wordcraft_doc::styles::TableStyleParts;
|
||||
|
|
@ -440,6 +441,11 @@ fn table_style_formatting_round_trips() {
|
|||
cell_margins: Some([1.0, 14.4, 0.0, 14.4]),
|
||||
header_chr: CharProps { italic: Some(true), ..Default::default() },
|
||||
header_fill: Some(Rgb(0xFF, 0xFF, 0)),
|
||||
header_borders: Some(Borders::all(Border::single(1.5))),
|
||||
band_fill: Some(Rgb(0xF2, 0xF2, 0xF2)),
|
||||
band_chr: CharProps { bold: Some(true), color: Some(TextColor::Rgb(Rgb(0, 0x70, 0xC0))), ..Default::default() },
|
||||
band_borders: Some(Borders::box_(Border { style: BorderStyle::None, width: 0.0, color: None, space: 0.0 })),
|
||||
band_size: Some(3),
|
||||
..Default::default()
|
||||
};
|
||||
d.styles.upsert(Style {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,8 @@ View|Macros|Record Macro|tools.recordMacro
|
|||
View|SharePoint|Properties|file.properties
|
||||
Table Design|Table Style Options|Table Style Options|table.look
|
||||
Table Design|Table Styles|Table Styles|table.style
|
||||
Table Design|Table Styles|New Table Style|table.newStyle
|
||||
Table Design|Table Styles|Modify Table Style|table.modifyStyle
|
||||
Table Design|Table Styles|Shading|table.shading
|
||||
Table Design|Borders|Borders|table.borders
|
||||
Table Design|Borders|Border Painter|table.borderPainter
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub mod references;
|
|||
pub mod review;
|
||||
pub mod speech;
|
||||
pub mod table;
|
||||
pub mod table_style;
|
||||
pub mod text;
|
||||
pub mod tools;
|
||||
pub mod view;
|
||||
|
|
@ -39,6 +40,7 @@ pub fn registry() -> Registry {
|
|||
v.extend(equation::specs());
|
||||
v.extend(page::specs());
|
||||
v.extend(table::specs());
|
||||
v.extend(table_style::specs());
|
||||
v.extend(review::specs());
|
||||
v.extend(file::specs());
|
||||
v.extend(design::specs());
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
t(CommandSpec::new("table.splitTable", "Split Table", "Table Layout › Merge", split_table)),
|
||||
t(CommandSpec::new("table.style", "Table Styles", "Table Design › Table Styles", |s, v| {
|
||||
let st = p::req_str(v, "style")?;
|
||||
let id = s.doc.styles.find(st).map(|x| x.id.clone()).ok_or_else(|| CmdError::Params(format!("no table style `{st}`")))?;
|
||||
let id = super::table_style::table_style_id(s, st).ok_or_else(|| CmdError::Params(format!("no table style `{st}`")))?;
|
||||
with_table(s, |t| t.props.style = Some(id.clone()))
|
||||
})
|
||||
.params(r#"{"style": string}"#)),
|
||||
|
|
@ -242,7 +242,6 @@ fn with_cells(s: &mut Session, f: impl Fn(&mut wordcraft_doc::Cell)) -> CmdResul
|
|||
sel_result(s)
|
||||
}
|
||||
|
||||
/// Bump the revision of every paragraph in a table (table style changes alter their layout).
|
||||
/// Table Layout › Text Direction: turn the selected cells' text. Without a value it cycles the
|
||||
/// caret cell's direction (horizontal → down → up) and gives every selected cell the result.
|
||||
fn text_direction(s: &mut Session, v: &Value) -> CmdResult {
|
||||
|
|
@ -260,7 +259,8 @@ fn text_direction(s: &mut Session, v: &Value) -> CmdResult {
|
|||
with_cells(s, |cl| cl.props.text_direction = dir)
|
||||
}
|
||||
|
||||
fn touch_cells(s: &mut Session, tp: &Path) -> Result<(), CmdError> {
|
||||
/// Bump the revision of every paragraph in a table (table style changes alter their layout).
|
||||
pub(crate) fn touch_cells(s: &mut Session, tp: &Path) -> Result<(), CmdError> {
|
||||
let story = s.sel.focus.story;
|
||||
let paths: Vec<Path> = s.doc.para_paths(story).into_iter().filter(|p| p.0.len() > tp.0.len() && p.0.starts_with(&tp.0)).collect();
|
||||
for p in paths {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
//! Table Design › Table Styles: create and modify table styles (whole table, header row and
|
||||
//! banded rows), stored as table styles in the document's style sheet (ECMA-376 §17.7.6).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use wordcraft_doc::props::{Border, BorderStyle, Borders, CharProps, ParaProps, Rgb, TextColor};
|
||||
use wordcraft_doc::styles::{Style, StyleKind, TableStyleParts};
|
||||
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||
|
||||
/// The formatting params both commands share; each region (`wholeTable`, `headerRow`,
|
||||
/// `bandedRows`) takes the same shortcuts.
|
||||
macro_rules! style_params {
|
||||
($head:literal) => {
|
||||
concat!(
|
||||
"{",
|
||||
$head,
|
||||
r#", "basedOn"?: string, "wholeTable"?: Region, "headerRow"?: Region, "bandedRows"?: Region, "bandSize"?: n, "para"?: ParaProps, "apply"?: bool}"#,
|
||||
r#" where Region = {"borders"?: bool | null (true: single lines, false: none, null: inherit), "borderColor"?: "RRGGBB", "borderWidth"?: pt, "fill"?: "RRGGBB" | null, "bold"?: bool, "italic"?: bool, "color"?: "RRGGBB" | null, "size"?: pt, "font"?: string}"#
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
CommandSpec::new("table.newStyle", "New Table Style", "Table Design › Table Styles", new_style).params(style_params!(r#""name": string"#)),
|
||||
CommandSpec::new("table.modifyStyle", "Modify Table Style", "Table Design › Table Styles", modify_style)
|
||||
.params(style_params!(r#""style"?: string (default: the current table's), "name"?: string"#)),
|
||||
]
|
||||
}
|
||||
|
||||
/// A table style's id by id or display name (case-insensitive); styles of other kinds don't count.
|
||||
pub fn table_style_id(s: &Session, name: &str) -> Option<String> {
|
||||
let st = &s.doc.styles.styles;
|
||||
st.iter()
|
||||
.find(|x| x.kind == StyleKind::Table && x.id == name)
|
||||
.or_else(|| st.iter().find(|x| x.kind == StyleKind::Table && x.name.eq_ignore_ascii_case(name)))
|
||||
.map(|x| x.id.clone())
|
||||
}
|
||||
|
||||
/// The style of the table at the caret.
|
||||
fn current_table_style(s: &Session) -> Option<String> {
|
||||
let (tp, _, _) = s.sel.focus.path.cell()?;
|
||||
s.doc.table(s.sel.focus.story, &tp)?.props.style.clone()
|
||||
}
|
||||
|
||||
fn new_style(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let name = p::req_str(v, "name")?.trim().to_string();
|
||||
if name.is_empty() {
|
||||
return Err(CmdError::Params("name is empty".into()));
|
||||
}
|
||||
if s.doc.styles.find(&name).is_some() {
|
||||
return Err(CmdError::Failed(format!("a style named `{name}` already exists")));
|
||||
}
|
||||
let based_on = match p::str(v, "basedOn") {
|
||||
Some(b) => Some(table_style_id(s, b).ok_or_else(|| CmdError::Params(format!("no table style `{b}`")))?),
|
||||
None => table_style_id(s, "TableGrid").or_else(|| table_style_id(s, "TableNormal")),
|
||||
};
|
||||
let id = s.doc.styles.new_id(&name);
|
||||
let mut st = Style { id: id.clone(), name, kind: StyleKind::Table, based_on, priority: Some(99), ..Default::default() };
|
||||
edit(&mut st, v)?;
|
||||
s.doc.styles.upsert(st);
|
||||
s.dirty = true;
|
||||
if p::bool(v, "apply").unwrap_or(true) {
|
||||
apply(s, &id)?;
|
||||
}
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
fn modify_style(s: &mut Session, v: &Value) -> CmdResult {
|
||||
let id = match p::str(v, "style") {
|
||||
Some(n) => table_style_id(s, n).ok_or_else(|| CmdError::Params(format!("no table style `{n}`")))?,
|
||||
None => current_table_style(s).ok_or_else(|| CmdError::Disabled("the cursor isn't in a table with a style".into()))?,
|
||||
};
|
||||
let rename = p::str(v, "name").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
|
||||
if let Some(n) = &rename
|
||||
&& s.doc.styles.find(n).is_some_and(|x| x.id != id)
|
||||
{
|
||||
return Err(CmdError::Failed(format!("a style named `{n}` already exists")));
|
||||
}
|
||||
let based_on = match p::str(v, "basedOn") {
|
||||
Some(b) => {
|
||||
let b = table_style_id(s, b).ok_or_else(|| CmdError::Params(format!("no table style `{b}`")))?;
|
||||
// A style can't be based on itself or on a style based on it.
|
||||
if s.doc.styles.chain(&b).iter().any(|x| x.id == id) {
|
||||
return Err(CmdError::Params("a style can't be based on itself".into()));
|
||||
}
|
||||
Some(b)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let mut st = s.doc.styles.get(&id).cloned().ok_or_else(|| CmdError::Params("no such style".into()))?;
|
||||
if let Some(n) = rename {
|
||||
st.name = n;
|
||||
}
|
||||
if let Some(b) = based_on {
|
||||
st.based_on = Some(b);
|
||||
}
|
||||
edit(&mut st, v)?;
|
||||
s.doc.styles.upsert(st);
|
||||
s.dirty = true;
|
||||
if p::bool(v, "apply").unwrap_or(false) {
|
||||
apply(s, &id)?;
|
||||
}
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
/// Apply style `id` to the table at the caret, if any.
|
||||
fn apply(s: &mut Session, id: &str) -> Result<(), CmdError> {
|
||||
let Some((tp, _, _)) = s.sel.focus.path.cell() else { return Ok(()) };
|
||||
s.doc.table_mut(s.sel.focus.story, &tp)?.props.style = Some(id.to_string());
|
||||
super::table::touch_cells(s, &tp)
|
||||
}
|
||||
|
||||
/// Apply the formatting params to `st`.
|
||||
fn edit(st: &mut Style, v: &Value) -> Result<(), CmdError> {
|
||||
let mut parts = st.table.take().unwrap_or_default();
|
||||
if let Some(r) = v.get("wholeTable") {
|
||||
region(r, &mut parts.fill, &mut st.chr, &mut parts.borders)?;
|
||||
}
|
||||
if let Some(r) = v.get("headerRow") {
|
||||
region(r, &mut parts.header_fill, &mut parts.header_chr, &mut parts.header_borders)?;
|
||||
}
|
||||
if let Some(r) = v.get("bandedRows") {
|
||||
region(r, &mut parts.band_fill, &mut parts.band_chr, &mut parts.band_borders)?;
|
||||
}
|
||||
if let Some(n) = v.get("bandSize") {
|
||||
parts.band_size = n.as_u64().map(|n| n.clamp(1, 1000) as u32);
|
||||
}
|
||||
if let Some(pp) = v.get("para") {
|
||||
let pp: ParaProps = serde_json::from_value(pp.clone()).map_err(|e| CmdError::Params(format!("para: {e}")))?;
|
||||
st.para.overlay(&pp);
|
||||
st.para.style = None;
|
||||
st.para.numbering = None;
|
||||
}
|
||||
st.table = (parts != TableStyleParts::default()).then_some(parts);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One region's shortcuts: borders, fill and character formatting.
|
||||
fn region(r: &Value, fill: &mut Option<Rgb>, chr: &mut CharProps, borders: &mut Option<Borders>) -> Result<(), CmdError> {
|
||||
if !r.is_object() {
|
||||
return Err(CmdError::Params("a region is an object".into()));
|
||||
}
|
||||
let color = |k: &str| -> Result<Option<Rgb>, CmdError> {
|
||||
match r.get(k) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(Value::String(h)) => Rgb::parse(h).map(Some).ok_or_else(|| CmdError::Params(format!("`{k}`: bad colour `{h}`"))),
|
||||
Some(_) => Err(CmdError::Params(format!("`{k}` is \"RRGGBB\" or null"))),
|
||||
}
|
||||
};
|
||||
if r.get("fill").is_some() {
|
||||
*fill = color("fill")?;
|
||||
}
|
||||
if r.get("color").is_some() {
|
||||
chr.color = color("color")?.map(TextColor::Rgb);
|
||||
}
|
||||
if let Some(b) = p::bool(r, "bold") {
|
||||
chr.bold = Some(b);
|
||||
chr.bold_cs = Some(b);
|
||||
}
|
||||
if let Some(b) = p::bool(r, "italic") {
|
||||
chr.italic = Some(b);
|
||||
chr.italic_cs = Some(b);
|
||||
}
|
||||
if let Some(sz) = p::f32(r, "size") {
|
||||
chr.size = Some(sz.clamp(1.0, 1638.0));
|
||||
chr.size_cs = chr.size;
|
||||
}
|
||||
if let Some(f) = p::str(r, "font").map(str::trim).filter(|f| !f.is_empty()) {
|
||||
chr.font = Some(f.to_string());
|
||||
}
|
||||
match r.get("borders") {
|
||||
Some(Value::Bool(true)) => {
|
||||
let width = p::f32(r, "borderWidth").unwrap_or(0.5).clamp(0.25, 6.0);
|
||||
*borders = Some(Borders::all(Border { style: BorderStyle::Single, width, color: color("borderColor")?, space: 0.0 }));
|
||||
}
|
||||
Some(Value::Bool(false)) => *borders = Some(Borders::all(Border { style: BorderStyle::None, width: 0.0, color: None, space: 0.0 })),
|
||||
Some(Value::Null) => *borders = None,
|
||||
Some(_) => return Err(CmdError::Params("`borders` is true, false or null".into())),
|
||||
None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1701,6 +1701,69 @@ fn timestamps_come_from_the_clock() {
|
|||
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
|
||||
}
|
||||
|
||||
/// #146: a table style made by command styles the table at the caret (header row fill and bold
|
||||
/// text, banded rows), can be modified, and saves as a table style the table refers to.
|
||||
#[test]
|
||||
fn custom_table_style_applies_modifies_and_saves() {
|
||||
use wordcraft_doc::{Rgb, StyleKind, TextColor};
|
||||
let mut s = s();
|
||||
run(&mut s, "insert.table", json!({"rows": 3, "cols": 2}));
|
||||
run(&mut s, "text.insert", json!({"text": "Head"}));
|
||||
run(&mut s, "table.look", json!({"headerRow": true, "bandedRows": true}));
|
||||
let (tp, _, _) = s.sel.focus.path.cell().unwrap();
|
||||
let r = run(
|
||||
&mut s,
|
||||
"table.newStyle",
|
||||
json!({"name": "Thesis Table", "basedOn": "Table Grid",
|
||||
"wholeTable": {"size": 10},
|
||||
"headerRow": {"fill": "1F3864", "bold": true, "color": "FFFFFF", "borders": true},
|
||||
"bandedRows": {"fill": "EEEEEE", "italic": true}}),
|
||||
);
|
||||
let id = r["id"].as_str().unwrap().to_string();
|
||||
let table_style = |s: &Session| s.doc.table(StoryRef::Body, &tp).unwrap().props.style.clone();
|
||||
assert_eq!(table_style(&s).as_deref(), Some(id.as_str()), "applied to the current table");
|
||||
assert!(s.run("table.newStyle", &json!({"name": "thesis table"})).is_err(), "names are unique");
|
||||
assert!(s.run("table.newStyle", &json!({"name": "X", "basedOn": "Heading 1"})).is_err(), "bases are table styles");
|
||||
assert!(s.run("table.style", &json!({"style": "Normal"})).is_err(), "only table styles apply to tables");
|
||||
|
||||
let fills = |s: &mut Session, c: Rgb| {
|
||||
s.layout().pages[0].items.iter().filter(|i| matches!(i, crate::layout::Placed::Fill { color, .. } if *color == c)).count()
|
||||
};
|
||||
let head = |s: &mut Session| {
|
||||
let mut p = tp.0.clone();
|
||||
p.extend([0, 0, 0]);
|
||||
s.layout().pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.find_map(
|
||||
|i| if let crate::layout::Placed::Lines { path, para, .. } = i { (path.0 == p).then(|| para.styles[0].rc.clone()) } else { None },
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(fills(&mut s, Rgb(0x1F, 0x38, 0x64)), 2, "header cells");
|
||||
assert_eq!(fills(&mut s, Rgb(0xEE, 0xEE, 0xEE)), 2, "first band");
|
||||
let rc = head(&mut s);
|
||||
assert!(rc.bold && rc.size == 10.0, "{rc:?}");
|
||||
assert_eq!(rc.color, TextColor::Rgb(Rgb::WHITE));
|
||||
|
||||
// Modify the current table's style: the table follows.
|
||||
run(&mut s, "table.modifyStyle", json!({"headerRow": {"fill": "C00000", "bold": false}, "bandSize": 2}));
|
||||
assert_eq!(fills(&mut s, Rgb(0x1F, 0x38, 0x64)), 0);
|
||||
assert_eq!(fills(&mut s, Rgb(0xC0, 0, 0)), 2);
|
||||
assert_eq!(fills(&mut s, Rgb(0xEE, 0xEE, 0xEE)), 4, "two rows per band");
|
||||
assert!(!head(&mut s).bold);
|
||||
|
||||
let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap();
|
||||
let st = back.styles.get(&id).unwrap();
|
||||
assert_eq!((st.kind, st.name.as_str(), st.based_on.as_deref()), (StyleKind::Table, "Thesis Table", Some("TableGrid")));
|
||||
let parts = st.table.as_ref().unwrap();
|
||||
assert_eq!((parts.header_fill, parts.band_fill, parts.band_size), (Some(Rgb(0xC0, 0, 0)), Some(Rgb(0xEE, 0xEE, 0xEE)), Some(2)));
|
||||
assert_eq!((parts.header_chr.bold, parts.band_chr.italic), (Some(false), Some(true)));
|
||||
assert!(parts.header_borders.is_some_and(|b| b.any_visible()));
|
||||
let t = back.body.iter().find_map(|b| b.as_table()).unwrap();
|
||||
assert_eq!(t.props.style.as_deref(), Some(id.as_str()), "w:tblStyle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_and_table_gridlines_toggle_independently() {
|
||||
// #69: View › Gridlines (the page drawing grid) and Table Layout › View Gridlines (table cell
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ struct Turned {
|
|||
across: f32,
|
||||
/// List counters and equation count before the cell, to lay it out again the same way.
|
||||
snap: (Counters, u32),
|
||||
/// Its formatting region (header row, total row, first column) and cell index in the row.
|
||||
region: (bool, bool, bool),
|
||||
/// Its formatting region (header row, total row, first column, row band) and cell index in the row.
|
||||
region: (bool, bool, bool, bool),
|
||||
ci: usize,
|
||||
/// Top margin plus top border band.
|
||||
top: f32,
|
||||
|
|
@ -97,8 +97,9 @@ fn first_cell_left_margin_in(t: &Table, def: [f32; 4]) -> f32 {
|
|||
pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], avail: f32, depth: usize) -> TableLayout {
|
||||
let style = table_style(ctx, t);
|
||||
let parts = style.as_ref().map(|s| &s.parts);
|
||||
// Cell text formatting per (header row, total row, first column) region, built once per table.
|
||||
let mut cell_text: Vec<((bool, bool, bool), CellText)> = Vec::new();
|
||||
// Cell text formatting per (header row, total row, first column, row band) region, built once
|
||||
// per table.
|
||||
let mut cell_text: Vec<((bool, bool, bool, bool), CellText)> = Vec::new();
|
||||
let ncols = t.cols().max(1);
|
||||
// Column widths.
|
||||
let mut grid: Vec<f32> = if t.grid.len() == ncols {
|
||||
|
|
@ -152,6 +153,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
|||
};
|
||||
let nrows = t.rows.len();
|
||||
let header_rows = t.props.look.header_row;
|
||||
let band_size = parts.and_then(|p| p.band_size).unwrap_or(1).clamp(1, 1000) as usize;
|
||||
// First pass: lay out every cell's content.
|
||||
struct CellBox {
|
||||
items: Vec<Placed>,
|
||||
|
|
@ -178,7 +180,16 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
|||
let mut row_insets = 0.0f32;
|
||||
let is_header = header_rows && ri == 0;
|
||||
let is_total = t.props.look.total_row && ri + 1 == nrows && nrows > 1;
|
||||
let band = t.props.look.banded_rows && !is_header && (ri - usize::from(header_rows)) % 2 == 0;
|
||||
let band = t.props.look.banded_rows && !is_header && (ri.saturating_sub(usize::from(header_rows)) / band_size).is_multiple_of(2);
|
||||
// The header row's or the odd band's own cell borders (conditional formatting).
|
||||
let region_borders = if is_header {
|
||||
parts.and_then(|p| p.header_borders)
|
||||
} else if band {
|
||||
parts.and_then(|p| p.band_borders)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.unwrap_or_default();
|
||||
for (ci, cell) in row.cells.iter().enumerate() {
|
||||
let span = cell.span();
|
||||
let x0 = colx.get(g).copied().unwrap_or(acc);
|
||||
|
|
@ -186,7 +197,7 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
|||
let margins = cell.props.margins.unwrap_or(margins_def);
|
||||
let cw = (x1 - x0 - margins[1] - margins[3]).max(4.0);
|
||||
let mut fill = cell.props.shading;
|
||||
let region = (is_header, is_total, t.props.look.first_column && g == 0);
|
||||
let region = (is_header, is_total, t.props.look.first_column && g == 0, band);
|
||||
if let Some(p) = parts {
|
||||
if is_header {
|
||||
fill = fill.or(p.header_fill);
|
||||
|
|
@ -202,6 +213,9 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
|||
// The whole table's formatting, then column, then row conditional formatting
|
||||
// (later regions win, ECMA-376 §17.7.6).
|
||||
let mut chr = st.chr.clone();
|
||||
if band {
|
||||
chr.overlay(&st.parts.band_chr);
|
||||
}
|
||||
if region.2 {
|
||||
chr.overlay(&st.parts.first_col_chr);
|
||||
}
|
||||
|
|
@ -221,11 +235,13 @@ pub fn layout_table(ctx: &mut Ctx, story: StoryRef, t: &Table, path: &[u32], ava
|
|||
let edge =
|
||||
|own: Option<Border>, outer: bool, outer_b: Option<Border>, inner_b: Option<Border>| own.or(if outer { outer_b } else { inner_b });
|
||||
let cb = cell.props.borders.unwrap_or_default();
|
||||
let rb = region_borders;
|
||||
let (first_g, last_g) = (g == 0, g + span >= ncols);
|
||||
let mut borders = Borders {
|
||||
top: edge(cb.top, ri == 0, tb.top, tb.between),
|
||||
bottom: edge(cb.bottom, ri + 1 == nrows, tb.bottom, tb.between),
|
||||
left: edge(cb.left, g == 0, tb.left, tb.inside_v),
|
||||
right: edge(cb.right, g + span >= ncols, tb.right, tb.inside_v),
|
||||
top: edge(cb.top.or(rb.top), ri == 0, tb.top, tb.between),
|
||||
bottom: edge(cb.bottom.or(rb.bottom), ri + 1 == nrows, tb.bottom, tb.between),
|
||||
left: edge(cb.left.or(if first_g { rb.left } else { rb.inside_v }), first_g, tb.left, tb.inside_v),
|
||||
right: edge(cb.right.or(if last_g { rb.right } else { rb.inside_v }), last_g, tb.right, tb.inside_v),
|
||||
between: None,
|
||||
inside_v: None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! Dialogs: Font, Paragraph, Find & Replace, Go To, Insert Table, Page Setup, Link, Bookmark,
|
||||
//! Word Count, Zoom, Watermark, New/Modify Style, Command search, About, Save Changes, and the
|
||||
//! mail-merge Recipient List, Insert Merge Field, Find Recipient, merge rules, Match Fields and
|
||||
//! Check for Errors. Every dialog ends by running a command (or shows one's result), so agents get
|
||||
//! the same result without the dialog.
|
||||
//! Word Count, Zoom, Watermark, New/Modify Style, New/Modify Table Style, Command search, About,
|
||||
//! Save Changes, and the mail-merge Recipient List, Insert Merge Field, Find Recipient, merge rules,
|
||||
//! Match Fields and Check for Errors. Every dialog ends by running a command (or shows one's
|
||||
//! result), so agents get the same result without the dialog.
|
||||
|
||||
use egui::{Sense, Ui, vec2};
|
||||
use serde::Serialize;
|
||||
|
|
@ -101,6 +101,21 @@ pub enum Dialog {
|
|||
before: f32,
|
||||
after: f32,
|
||||
},
|
||||
/// Table Design › New / Modify Table Style: one formatting region at a time.
|
||||
TableStyle {
|
||||
/// The style being modified; `None` creates one.
|
||||
id: Option<String>,
|
||||
name: String,
|
||||
/// Style id.
|
||||
based_on: String,
|
||||
/// Index into `regions`: whole table, header row, banded rows.
|
||||
region: usize,
|
||||
regions: Box<[TableRegion; 3]>,
|
||||
/// What the regions showed when the dialog opened (or the base style changed): only
|
||||
/// changes are sent, so everything else stays inherited.
|
||||
#[serde(skip)]
|
||||
basis: Box<[TableRegion; 3]>,
|
||||
},
|
||||
Commands {
|
||||
query: String,
|
||||
},
|
||||
|
|
@ -211,6 +226,93 @@ pub fn recipient_table(fields: &[String], rows: &[Vec<String>]) -> Result<Value,
|
|||
Ok(json!({"fields": fields, "rows": rows}))
|
||||
}
|
||||
|
||||
/// One table style region in the Table Style dialog: colours are hex, empty for none.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TableRegion {
|
||||
pub borders: bool,
|
||||
pub fill: String,
|
||||
pub bold: bool,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
/// The regions of table style `id` as resolved through its based-on chain.
|
||||
fn table_regions(app: &WordApp, id: &str) -> Box<[TableRegion; 3]> {
|
||||
let Some(st) = app.session.doc.styles.table_style(id) else { return Default::default() };
|
||||
let p = &st.parts;
|
||||
let hex = |c: Option<wordcraft_doc::Rgb>| c.map(|c| c.hex()).unwrap_or_default();
|
||||
let text = |c: &wordcraft_doc::CharProps| match c.color {
|
||||
Some(wordcraft_doc::TextColor::Rgb(c)) => c.hex(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let lines = |b: Option<wordcraft_doc::props::Borders>| b.is_some_and(|b| b.any_visible());
|
||||
let mut header_chr = st.chr.clone();
|
||||
header_chr.overlay(&p.header_chr);
|
||||
let mut band_chr = st.chr.clone();
|
||||
band_chr.overlay(&p.band_chr);
|
||||
Box::new([
|
||||
TableRegion { borders: lines(p.borders), fill: hex(p.fill), bold: st.chr.bold.unwrap_or(false), color: text(&st.chr) },
|
||||
TableRegion { borders: lines(p.header_borders), fill: hex(p.header_fill), bold: header_chr.bold.unwrap_or(false), color: text(&header_chr) },
|
||||
TableRegion { borders: lines(p.band_borders), fill: hex(p.band_fill), bold: band_chr.bold.unwrap_or(false), color: text(&band_chr) },
|
||||
])
|
||||
}
|
||||
|
||||
/// The style of the table at the caret, when it is a table style.
|
||||
fn current_table_style(app: &WordApp) -> Option<String> {
|
||||
let s = &app.session;
|
||||
let (tp, _, _) = s.sel.focus.path.cell()?;
|
||||
let id = s.doc.table(s.sel.focus.story, &tp)?.props.style.clone()?;
|
||||
s.doc.styles.get(&id).filter(|st| st.kind == wordcraft_doc::StyleKind::Table).map(|st| st.id.clone())
|
||||
}
|
||||
|
||||
/// The params `table.newStyle` / `table.modifyStyle` need for what changed in one region.
|
||||
fn region_changes(r: &TableRegion, basis: &TableRegion) -> Value {
|
||||
let color = |c: &str| if c.is_empty() { Value::Null } else { json!(c) };
|
||||
let mut v = json!({});
|
||||
if r.borders != basis.borders {
|
||||
v["borders"] = json!(r.borders);
|
||||
}
|
||||
if r.fill != basis.fill {
|
||||
v["fill"] = color(&r.fill);
|
||||
}
|
||||
if r.bold != basis.bold {
|
||||
v["bold"] = json!(r.bold);
|
||||
}
|
||||
if r.color != basis.color {
|
||||
v["color"] = color(&r.color);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// A colour menu: a swatch with the colour grid and No Color. `value` is hex, empty for none.
|
||||
fn color_menu(ui: &mut Ui, theme: &[wordcraft_doc::Rgb], value: &mut String) {
|
||||
let c = wordcraft_doc::Rgb::parse(value);
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
|
||||
let t = Tokens::get(ui.ctx());
|
||||
match c {
|
||||
Some(c) => {
|
||||
ui.painter().rect_filled(r, 2.0, crate::theme::c32(c));
|
||||
}
|
||||
None => {
|
||||
ui.painter().line_segment([r.left_bottom(), r.right_top()], egui::Stroke::new(1.0, t.text_dim));
|
||||
}
|
||||
}
|
||||
ui.painter().rect_stroke(r, 2.0, egui::Stroke::new(1.0, t.border_strong), egui::StrokeKind::Inside);
|
||||
let label = if c.is_some() { value.clone() } else { tl!("No Color").to_string() };
|
||||
ui.menu_button(label, |ui| {
|
||||
if ui.button(tl!("No Color")).clicked() {
|
||||
value.clear();
|
||||
ui.close();
|
||||
}
|
||||
if let Some(hex) = crate::widgets::color_grid(ui, theme) {
|
||||
*value = hex;
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -228,6 +330,8 @@ impl Dialog {
|
|||
Dialog::Watermark { .. } => "watermark",
|
||||
Dialog::NewStyle { .. } => "newStyle",
|
||||
Dialog::ModifyStyle { .. } => "modifyStyle",
|
||||
Dialog::TableStyle { id: None, .. } => "newTableStyle",
|
||||
Dialog::TableStyle { .. } => "modifyTableStyle",
|
||||
Dialog::Commands { .. } => "commands",
|
||||
Dialog::About { .. } => "about",
|
||||
Dialog::SaveChanges { .. } => "saveChanges",
|
||||
|
|
@ -327,6 +431,20 @@ impl Dialog {
|
|||
"zoom" => Dialog::Zoom { percent: (app.session.view.zoom * 100.0).round() },
|
||||
"watermark" => Dialog::Watermark { text: "CONFIDENTIAL".into(), diagonal: true },
|
||||
"newStyle" => Dialog::NewStyle { name: "Style1".into(), based_on: "Normal".into() },
|
||||
"newTableStyle" => {
|
||||
let based_on = current_table_style(app).unwrap_or_else(|| "TableGrid".into());
|
||||
let styles = &app.session.doc.styles;
|
||||
let name = (1..1000).map(|i| format!("Table Style {i}")).find(|n| styles.find(n).is_none()).unwrap_or_default();
|
||||
let regions = table_regions(app, &based_on);
|
||||
Dialog::TableStyle { id: None, name, based_on, region: 0, basis: regions.clone(), regions }
|
||||
}
|
||||
"modifyTableStyle" => {
|
||||
let id = current_table_style(app)?;
|
||||
let st = app.session.doc.styles.get(&id)?;
|
||||
let (name, based_on) = (st.name.clone(), st.based_on.clone().unwrap_or_default());
|
||||
let regions = table_regions(app, &id);
|
||||
Dialog::TableStyle { id: Some(id), name, based_on, region: 0, basis: regions.clone(), regions }
|
||||
}
|
||||
"commands" => Dialog::Commands { query: String::new() },
|
||||
"about" => Dialog::About { tab: 0 },
|
||||
"contributors" => Dialog::About { tab: 1 },
|
||||
|
|
@ -449,6 +567,8 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) {
|
|||
Dialog::Watermark { .. } => "Custom Watermark",
|
||||
Dialog::NewStyle { .. } => "Create New Style",
|
||||
Dialog::ModifyStyle { .. } => "Modify Style",
|
||||
Dialog::TableStyle { id: None, .. } => "New Table Style",
|
||||
Dialog::TableStyle { .. } => "Modify Table Style",
|
||||
Dialog::Commands { .. } => "Search Commands",
|
||||
Dialog::About { .. } => "About WordCraft",
|
||||
Dialog::SaveChanges { .. } => "WordCraft",
|
||||
|
|
@ -883,6 +1003,90 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
|||
}
|
||||
ok || cancel
|
||||
}
|
||||
Dialog::TableStyle { id, name, based_on, region, regions, basis } => {
|
||||
let styles: Vec<(String, String)> = app
|
||||
.session
|
||||
.doc
|
||||
.styles
|
||||
.styles
|
||||
.iter()
|
||||
.filter(|s| s.kind == wordcraft_doc::StyleKind::Table && Some(&s.id) != id.as_ref())
|
||||
.map(|s| (s.id.clone(), s.name.clone()))
|
||||
.collect();
|
||||
let base_name = styles.iter().find(|(i, _)| i == based_on).map(|(_, n)| n.clone()).unwrap_or_else(|| based_on.clone());
|
||||
let mut rebased = false;
|
||||
egui::Grid::new("tstyle").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||
ui.label(tl!("Name:"));
|
||||
ui.text_edit_singleline(name);
|
||||
ui.end_row();
|
||||
ui.label(tl!("Style based on:"));
|
||||
egui::ComboBox::from_id_salt("tstyle_base").width(200.0).selected_text(base_name).show_ui(ui, |ui| {
|
||||
for (sid, sname) in &styles {
|
||||
if ui.selectable_label(sid == based_on, sname).clicked() && sid != based_on {
|
||||
*based_on = sid.clone();
|
||||
rebased = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Apply formatting to:"));
|
||||
let names = ["Whole Table", "Header Row", "Banded Rows"];
|
||||
egui::ComboBox::from_id_salt("tstyle_region")
|
||||
.width(200.0)
|
||||
.selected_text(tl!(names.get(*region).copied().unwrap_or("Whole Table")))
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, n) in names.iter().enumerate() {
|
||||
ui.selectable_value(region, i, tl!(n));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
// A new style shows its new base's formatting; a modified one keeps its own.
|
||||
if rebased && id.is_none() {
|
||||
*regions = table_regions(app, based_on);
|
||||
*basis = regions.clone();
|
||||
}
|
||||
ui.separator();
|
||||
let theme = app.session.doc.settings.theme_colors.clone();
|
||||
if let Some(r) = regions.get_mut(*region) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut r.borders, tl!("Borders"));
|
||||
ui.checkbox(&mut r.bold, tl!("Bold"));
|
||||
});
|
||||
egui::Grid::new("tstyle_colors").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||
ui.label(tl!("Fill color:"));
|
||||
color_menu(ui, &theme, &mut r.fill);
|
||||
ui.end_row();
|
||||
ui.label(tl!("Font color:"));
|
||||
color_menu(ui, &theme, &mut r.color);
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
let (ok, cancel) = buttons(ui, tl!("OK"));
|
||||
if ok {
|
||||
let mut v = json!({"name": name.trim(), "basedOn": based_on});
|
||||
for (i, key) in ["wholeTable", "headerRow", "bandedRows"].into_iter().enumerate() {
|
||||
if let (Some(r), Some(b)) = (regions.get(i), basis.get(i)) {
|
||||
let ch = region_changes(r, b);
|
||||
if ch.as_object().is_some_and(|o| !o.is_empty()) {
|
||||
v[key] = ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
let cmd = match id {
|
||||
Some(sid) => {
|
||||
v["style"] = json!(sid);
|
||||
"table.modifyStyle"
|
||||
}
|
||||
None => "table.newStyle",
|
||||
};
|
||||
if let Err(e) = app.run(cmd, v) {
|
||||
app.status(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ok || cancel
|
||||
}
|
||||
Dialog::Commands { query } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(query).hint_text(tl!("Type a command, e.g. \"insert table\"")).desired_width(380.0));
|
||||
r.request_focus();
|
||||
|
|
|
|||
|
|
@ -948,3 +948,10 @@
|
|||
Dark Oscuro
|
||||
Use system setting Usar la configuración del sistema
|
||||
A file dialog is already open. Ya hay un cuadro de diálogo de archivo abierto.
|
||||
New Table Style Nuevo estilo de tabla
|
||||
Modify Table Style Modificar estilo de tabla
|
||||
New Table Style… Nuevo estilo de tabla…
|
||||
Modify Table Style… Modificar estilo de tabla…
|
||||
Apply formatting to: Aplicar formato a:
|
||||
Whole Table Toda la tabla
|
||||
Fill color: Color de relleno:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -931,3 +931,10 @@
|
|||
Dark ダーク
|
||||
Use system setting システム設定を使用
|
||||
A file dialog is already open. ファイル ダイアログが既に開いています。
|
||||
New Table Style 新しい表スタイル
|
||||
Modify Table Style 表スタイルの変更
|
||||
New Table Style… 新しい表スタイル…
|
||||
Modify Table Style… 表スタイルの変更…
|
||||
Apply formatting to: 書式の適用先:
|
||||
Whole Table 表全体
|
||||
Fill color: 塗りつぶしの色:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -943,3 +943,10 @@
|
|||
Dark Escuro
|
||||
Use system setting Usar a configuração do sistema
|
||||
A file dialog is already open. Já há uma caixa de diálogo de arquivo aberta.
|
||||
New Table Style Novo estilo de tabela
|
||||
Modify Table Style Modificar estilo de tabela
|
||||
New Table Style… Novo estilo de tabela…
|
||||
Modify Table Style… Modificar estilo de tabela…
|
||||
Apply formatting to: Aplicar formatação a:
|
||||
Whole Table Tabela inteira
|
||||
Fill color: Cor de preenchimento:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -943,3 +943,10 @@
|
|||
Dark Темна
|
||||
Use system setting Як у системі
|
||||
A file dialog is already open. Діалогове вікно файлу вже відкрито.
|
||||
New Table Style Новий стиль таблиці
|
||||
Modify Table Style Змінити стиль таблиці
|
||||
New Table Style… Новий стиль таблиці…
|
||||
Modify Table Style… Змінити стиль таблиці…
|
||||
Apply formatting to: Застосувати форматування до:
|
||||
Whole Table Уся таблиця
|
||||
Fill color: Колір заливки:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
|
|
@ -942,3 +942,10 @@
|
|||
Dark 深色
|
||||
Use system setting 使用系统设置
|
||||
A file dialog is already open. 已有一个文件对话框处于打开状态。
|
||||
New Table Style 新建表格样式
|
||||
Modify Table Style 修改表格样式
|
||||
New Table Style… 新建表格样式…
|
||||
Modify Table Style… 修改表格样式…
|
||||
Apply formatting to: 将格式应用于:
|
||||
Whole Table 整个表格
|
||||
Fill color: 填充颜色:
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -932,3 +932,10 @@
|
|||
Dark 深色
|
||||
Use system setting 使用系統設定
|
||||
A file dialog is already open. 已有一個檔案對話方塊開啟中。
|
||||
New Table Style 新增表格樣式
|
||||
Modify Table Style 修改表格樣式
|
||||
New Table Style… 新增表格樣式…
|
||||
Modify Table Style… 修改表格樣式…
|
||||
Apply formatting to: 將格式套用至:
|
||||
Whole Table 整個表格
|
||||
Fill color: 填滿色彩:
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
|
@ -329,7 +329,9 @@ pub fn table_style_tile(ui: &mut Ui, app: &mut WordApp, style: &str) -> Response
|
|||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 3.0, t.hover);
|
||||
}
|
||||
let key = format!("tstyle:{style}:{ppp}");
|
||||
// Keyed by the style sheet too, so a modified table style redraws.
|
||||
let skey = app.previews.styles_key(&app.session.doc, app.session.rev());
|
||||
let key = format!("tstyle:{style}:{skey}:{ppp}");
|
||||
let doc = &app.session.doc;
|
||||
let tex = app.previews.get_or(ui.ctx(), &key, || {
|
||||
let mut tb = Table::new(5, 4, 100.0);
|
||||
|
|
|
|||
|
|
@ -1257,6 +1257,22 @@ fn table_design(app: &mut WordApp, ui: &mut Ui) {
|
|||
});
|
||||
});
|
||||
stack(ui, |ui| {
|
||||
let can_modify = look.is_some()
|
||||
&& app
|
||||
.session
|
||||
.sel
|
||||
.focus
|
||||
.path
|
||||
.cell()
|
||||
.and_then(|(tp, _, _)| app.session.doc.table(app.session.sel.focus.story, &tp)?.props.style.clone())
|
||||
.is_some();
|
||||
menu_button(ui, app, "styles", Some("Styles"), "Table Styles", false, |ui, app| {
|
||||
mi(ui, app, "New Table Style…", "ui.dialog", json!({"name": "newTableStyle"}));
|
||||
if ui.add_enabled(can_modify, egui::Button::new(tl!("Modify Table Style…")).min_size(vec2(200.0, 0.0))).clicked() {
|
||||
let _ = app.run("ui.dialog", json!({"name": "modifyTableStyle"}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry.
|
||||
|
||||
**378 of 428 catalog features have commands (88.3%).**
|
||||
**380 of 430 catalog features have commands (88.4%).**
|
||||
|
||||
| Tab | Live | Total |
|
||||
|---|---|---|
|
||||
|
|
@ -17,7 +17,7 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-
|
|||
| Mailings | 17 | 17 |
|
||||
| Review | 26 | 29 |
|
||||
| View | 25 | 29 |
|
||||
| Table Design | 5 | 5 |
|
||||
| Table Design | 7 | 7 |
|
||||
| Table Layout | 28 | 30 |
|
||||
| Equation | 21 | 22 |
|
||||
| Picture Format | 13 | 13 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue