mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-11 21:02:45 +00:00
S: table and cell styles (style.cell.*, style.table.*), Table panel pickers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
08727dfd7e
commit
e433c77b55
7 files changed
+488
-8
No files matched your search
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Table and cell styles: cell styles (fill, insets, vertical justification, strokes, paragraph style) and table styles (region cell styles, border, alternating rows, spacing); apply from the Table panel, edits re-apply to every user; `style.cell.*`, `style.table.*`. IDML mapping is still to come.
|
||||
- Style groups: paragraph and character styles shown in group folders, Move to Group (new, existing, none); `style.group` renames every use (styles, stories, table cells, footnotes, object styles).
|
||||
- Scrubby zoom: drag the Zoom tool left or right to zoom continuously around the press point.
|
||||
- Guides belong to the active layer: hidden with it (or with its Show Guides off) and locked with it.
|
||||
|
||||
@@ -63,6 +63,11 @@ pub struct Styles {
|
||||
pub paragraph: Vec<ParagraphStyle>,
|
||||
pub character: Vec<CharacterStyle>,
|
||||
pub object: Vec<ObjectStyle>,
|
||||
/// Cell and table styles (Table › Cell Styles / Table Styles).
|
||||
#[serde(default)]
|
||||
pub cell: Vec<CellStyle>,
|
||||
#[serde(default)]
|
||||
pub table: Vec<TableStyle>,
|
||||
/// Default styles for new text/frames (the style selected with nothing selected).
|
||||
pub default_paragraph: String,
|
||||
pub default_character: String,
|
||||
@@ -103,6 +108,8 @@ impl Default for Styles {
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
cell: vec![CellStyle { name: NO_CELL_STYLE.into(), ..Default::default() }],
|
||||
table: vec![TableStyle { name: BASIC_TABLE.into(), ..Default::default() }],
|
||||
default_paragraph: BASIC_PARAGRAPH.into(),
|
||||
default_character: NO_CHAR_STYLE.into(),
|
||||
default_text_frame: BASIC_TEXT_FRAME.into(),
|
||||
@@ -111,6 +118,120 @@ impl Default for Styles {
|
||||
}
|
||||
}
|
||||
|
||||
pub const NO_CELL_STYLE: &str = "[None]";
|
||||
pub const BASIC_TABLE: &str = "[Basic Table]";
|
||||
|
||||
/// A cell style: the cell attributes it sets (unset = left as is) and a paragraph style for the
|
||||
/// cell's text.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct CellStyle {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fill: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fill_tint: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub insets: Option<[f64; 4]>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vj: Option<crate::item::VerticalJustification>,
|
||||
/// All four edges.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stroke: Option<crate::table::CellStroke>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub paragraph_style: Option<String>,
|
||||
}
|
||||
|
||||
/// A table style: cell styles per region, the border, alternating row fills and spacing.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct TableStyle {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub footer: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub left_column: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub right_column: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border: Option<crate::table::CellStroke>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alt_rows: Option<crate::table::AltFills>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub space_before: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub space_after: Option<f64>,
|
||||
}
|
||||
|
||||
impl CellStyle {
|
||||
/// Write this style's attributes into `cell`.
|
||||
pub fn apply_to(&self, cell: &mut crate::table::Cell) {
|
||||
if let Some(v) = &self.fill {
|
||||
cell.fill = v.clone();
|
||||
}
|
||||
if let Some(v) = self.fill_tint {
|
||||
cell.fill_tint = v;
|
||||
}
|
||||
if let Some(v) = self.insets {
|
||||
cell.insets = v;
|
||||
}
|
||||
if let Some(v) = self.vj {
|
||||
cell.vj = v;
|
||||
}
|
||||
if let Some(v) = &self.stroke {
|
||||
cell.strokes = [v.clone(), v.clone(), v.clone(), v.clone()];
|
||||
}
|
||||
if let Some(ps) = &self.paragraph_style {
|
||||
for p in &mut cell.text.paras {
|
||||
p.style = ps.clone();
|
||||
}
|
||||
cell.text.rev += 1;
|
||||
}
|
||||
cell.style = if self.name == NO_CELL_STYLE { String::new() } else { self.name.clone() };
|
||||
}
|
||||
}
|
||||
|
||||
impl TableStyle {
|
||||
/// Write this style into `t`: options, then each cell's region style.
|
||||
pub fn apply_to(&self, t: &mut crate::table::Table, cells: &[CellStyle]) {
|
||||
if let Some(b) = &self.border {
|
||||
t.options.border = b.clone();
|
||||
}
|
||||
if self.alt_rows.is_some() {
|
||||
t.options.alt_rows = self.alt_rows.clone();
|
||||
}
|
||||
if let Some(v) = self.space_before {
|
||||
t.options.space_before = v;
|
||||
}
|
||||
if let Some(v) = self.space_after {
|
||||
t.options.space_after = v;
|
||||
}
|
||||
let (nr, nc) = (t.rows.len(), t.columns.len());
|
||||
for r in 0..nr {
|
||||
let kind = t.rows[r].kind;
|
||||
for c in 0..nc {
|
||||
let region = match kind {
|
||||
crate::table::RowKind::Header => self.header.as_ref(),
|
||||
crate::table::RowKind::Footer => self.footer.as_ref(),
|
||||
_ if c == 0 && self.left_column.is_some() => self.left_column.as_ref(),
|
||||
_ if c + 1 == nc && self.right_column.is_some() => self.right_column.as_ref(),
|
||||
_ => self.body.as_ref(),
|
||||
};
|
||||
if let (Some(name), Some(cell)) = (region, t.cells.get_mut(r * nc + c))
|
||||
&& let Some(cs) = cells.iter().find(|s| s.name == *name)
|
||||
{
|
||||
cs.apply_to(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
t.style = if self.name == BASIC_TABLE { String::new() } else { self.name.clone() };
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_DEPTH: usize = 32;
|
||||
|
||||
impl Styles {
|
||||
|
||||
+15
-1
@@ -116,6 +116,9 @@ pub struct Cell {
|
||||
/// Edge strokes: top, left, bottom, right.
|
||||
#[serde(default)]
|
||||
pub strokes: [CellStroke; 4],
|
||||
/// Applied cell style ("" = [None]).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub style: String,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
@@ -130,6 +133,7 @@ impl Default for Cell {
|
||||
vj: VerticalJustification::Top,
|
||||
rotation: 0.0,
|
||||
strokes: Default::default(),
|
||||
style: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +240,9 @@ pub struct Table {
|
||||
pub cells: Vec<Cell>,
|
||||
#[serde(default)]
|
||||
pub options: TableOptions,
|
||||
/// Applied table style ("" = [Basic Table]).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub style: String,
|
||||
}
|
||||
|
||||
/// A rectangular range of cells (inclusive).
|
||||
@@ -270,7 +277,14 @@ impl Table {
|
||||
rows.extend((0..footer).map(|_| Row { kind: RowKind::Footer, ..Default::default() }));
|
||||
let w = (width / cols as f64).max(3.0);
|
||||
let n = rows.len() * cols;
|
||||
Table { id, rows, columns: vec![Column { width: w }; cols], cells: vec![Cell::default(); n], options: TableOptions::default() }
|
||||
Table {
|
||||
id,
|
||||
rows,
|
||||
columns: vec![Column { width: w }; cols],
|
||||
cells: vec![Cell::default(); n],
|
||||
options: TableOptions::default(),
|
||||
style: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A table from rows of cell strings (Convert Text to Table). Short rows are padded.
|
||||
|
||||
@@ -38,6 +38,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"paragraph": st.doc.styles.paragraph.iter().map(|p| &p.name).collect::<Vec<_>>(),
|
||||
"character": st.doc.styles.character.iter().map(|p| &p.name).collect::<Vec<_>>(),
|
||||
"object": st.doc.styles.object.iter().map(|p| &p.name).collect::<Vec<_>>(),
|
||||
"cell": st.doc.styles.cell.iter().map(|p| &p.name).collect::<Vec<_>>(),
|
||||
"table": st.doc.styles.table.iter().map(|p| &p.name).collect::<Vec<_>>(),
|
||||
}))
|
||||
}),
|
||||
cmd!("style.object.apply", "Apply Object Style", [], None, "{name, ids?}", super::has_selection, apply_object),
|
||||
|
||||
@@ -17,6 +17,63 @@ use super::{CommandSpec, bad, cmd, f64_or, has_doc, has_text, ok, str_param};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
let mut v = style_specs();
|
||||
v.extend(table_specs());
|
||||
v
|
||||
}
|
||||
|
||||
fn style_specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"style.cell.create",
|
||||
"New Cell Style…",
|
||||
[],
|
||||
None,
|
||||
"{name, fromSelection?: bool (the target cell's look), fill?, tint?, insets?: n|[t,l,b,r], vj?: top|center|bottom|justify, stroke?: {weight, color, tint?}, paragraphStyle?} → {name}",
|
||||
has_doc,
|
||||
|s, p| cell_style_create(s, p)
|
||||
),
|
||||
cmd!(
|
||||
"style.cell.apply",
|
||||
"Apply Cell Style",
|
||||
[],
|
||||
None,
|
||||
"{name, story?, table?} — the target cells (the whole table when the cursor is in it)",
|
||||
in_table,
|
||||
cell_style_apply
|
||||
),
|
||||
cmd!(
|
||||
"style.cell.edit",
|
||||
"Cell Style Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, …same as style.cell.create} — cells using the style follow",
|
||||
has_doc,
|
||||
cell_style_edit
|
||||
),
|
||||
cmd!(
|
||||
"style.table.create",
|
||||
"New Table Style…",
|
||||
[],
|
||||
None,
|
||||
"{name, header?, body?, footer?, leftColumn?, rightColumn?: cell style names, border?: {weight, color}, altRows?: {first, firstColor, next, nextColor}, spaceBefore?, spaceAfter?} → {name}",
|
||||
has_doc,
|
||||
table_style_create
|
||||
),
|
||||
cmd!("style.table.apply", "Apply Table Style", [], None, "{name, story?, table?}", in_table, table_style_apply),
|
||||
cmd!(
|
||||
"style.table.edit",
|
||||
"Table Style Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, …same as style.table.create} — tables using the style follow",
|
||||
has_doc,
|
||||
table_style_edit
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn table_specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"table.insert",
|
||||
@@ -730,12 +787,270 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.map(|(r, c)| {
|
||||
let cell = t.cell(r, c).expect("in range");
|
||||
json!({"row": r, "col": c, "rowSpan": cell.row_span, "colSpan": cell.col_span, "text": cell.text.text, "fill": cell.fill,
|
||||
"insets": cell.insets, "vj": cell.vj})
|
||||
"insets": cell.insets, "vj": cell.vj, "style": cell.style})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({
|
||||
"story": g.story.0, "table": g.table, "anchor": st.doc.story(g.story).and_then(|x| x.table_anchor(g.table)),
|
||||
"rows": t.rows, "columns": t.columns, "headerRows": t.header_rows(), "footerRows": t.footer_rows(),
|
||||
"cells": cells, "options": t.options, "range": g.range,
|
||||
"cells": cells, "options": t.options, "range": g.range, "style": t.style,
|
||||
}))
|
||||
}
|
||||
|
||||
// ---------- cell and table styles ----------
|
||||
|
||||
fn stroke_param(v: Option<&Value>) -> Option<designcraft_doc::CellStroke> {
|
||||
let o = v?.as_object()?;
|
||||
let mut st = designcraft_doc::CellStroke::default();
|
||||
if let Some(w) = o.get("weight").and_then(Value::as_f64) {
|
||||
st.weight = w.max(0.0);
|
||||
}
|
||||
if let Some(c) = o.get("color").and_then(Value::as_str) {
|
||||
st.color = c.to_string();
|
||||
}
|
||||
if let Some(t) = o.get("tint").and_then(Value::as_f64) {
|
||||
st.tint = t.clamp(0.0, 1.0) as f32;
|
||||
}
|
||||
Some(st)
|
||||
}
|
||||
|
||||
/// Fill in the cell style fields given in `p`.
|
||||
fn cell_style_fields(cs: &mut designcraft_doc::CellStyle, p: &Value, cmd: &str) -> Result<()> {
|
||||
if let Some(f) = color_param(p, "fill") {
|
||||
cs.fill = Some(f);
|
||||
}
|
||||
if let Some(t) = p.get("tint").and_then(Value::as_f64) {
|
||||
cs.fill_tint = Some(t.clamp(0.0, 1.0) as f32);
|
||||
}
|
||||
match p.get("insets") {
|
||||
Some(Value::Number(n)) => cs.insets = n.as_f64().map(|v| [v.max(0.0); 4]),
|
||||
Some(Value::Array(a)) if a.len() == 4 => {
|
||||
let v: Vec<f64> = a.iter().map(|x| x.as_f64().unwrap_or(0.0).max(0.0)).collect();
|
||||
cs.insets = Some([v[0], v[1], v[2], v[3]]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(v) = p.get("vj") {
|
||||
cs.vj = Some(serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("vj: {e}")))?);
|
||||
}
|
||||
if let Some(st) = stroke_param(p.get("stroke")) {
|
||||
cs.stroke = Some(st);
|
||||
}
|
||||
if let Some(ps) = str_param(p, "paragraphStyle") {
|
||||
cs.paragraph_style = Some(ps.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cell_style_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base = str_param(p, "name").unwrap_or("Cell Style 1").to_string();
|
||||
let mut cs = designcraft_doc::CellStyle::default();
|
||||
if p.get("fromSelection").and_then(Value::as_bool).unwrap_or(false) {
|
||||
let g = target(s, p, "style.cell.create")?;
|
||||
let d = &s.doc()?.doc;
|
||||
let cell = d
|
||||
.story(g.story)
|
||||
.and_then(|st| st.tables.get(&g.table))
|
||||
.and_then(|t| t.cell(g.range.r0, g.range.c0))
|
||||
.ok_or_else(|| bad("style.cell.create", "no cell"))?;
|
||||
cs.fill = Some(cell.fill.clone());
|
||||
cs.fill_tint = Some(cell.fill_tint);
|
||||
cs.insets = Some(cell.insets);
|
||||
cs.vj = Some(cell.vj);
|
||||
cs.stroke = Some(cell.strokes[0].clone());
|
||||
cs.paragraph_style = cell.text.paras.first().map(|p| p.style.clone());
|
||||
}
|
||||
cell_style_fields(&mut cs, p, "style.cell.create")?;
|
||||
s.edit(|d, _| {
|
||||
let name = designcraft_doc::Styles::unique_name(|n| d.styles.cell.iter().any(|c| c.name == n), &base);
|
||||
cs.name = name.clone();
|
||||
d.styles_mut().cell.push(cs);
|
||||
Ok(json!({"name": name}))
|
||||
})
|
||||
}
|
||||
|
||||
fn cell_style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.cell.apply", "missing name"))?.to_string();
|
||||
let cs = s
|
||||
.doc()?
|
||||
.doc
|
||||
.styles
|
||||
.cell
|
||||
.iter()
|
||||
.find(|c| c.name == name)
|
||||
.cloned()
|
||||
.ok_or_else(|| bad("style.cell.apply", format!("no cell style `{name}`")))?;
|
||||
let g = target(s, p, "style.cell.apply")?;
|
||||
edit_table(s, &g, "style.cell.apply", |t| {
|
||||
let owners = t.owners();
|
||||
let nc = t.ncols();
|
||||
let mut n = 0;
|
||||
for r in g.range.r0..=g.range.r1 {
|
||||
for c in g.range.c0..=g.range.c1 {
|
||||
if owners[r * nc + c] == (r, c)
|
||||
&& let Some(cell) = t.cell_mut(r, c)
|
||||
{
|
||||
cs.apply_to(cell);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"cells": n}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Re-apply cell styles named in `names` to every cell using them (after a style edit).
|
||||
fn reapply_cell_styles(d: &mut Document, names: &[String]) {
|
||||
let styles = d.styles.cell.clone();
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
let Some(st) = d.story_mut(sid) else { continue };
|
||||
for t in st.tables.values_mut() {
|
||||
if !t.cells.iter().any(|c| names.contains(&c.style)) {
|
||||
continue;
|
||||
}
|
||||
for cell in &mut std::sync::Arc::make_mut(t).cells {
|
||||
if names.contains(&cell.style)
|
||||
&& let Some(cs) = styles.iter().find(|x| x.name == cell.style)
|
||||
{
|
||||
cs.apply_to(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
st.rev += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_style_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.cell.edit", "missing name"))?.to_string();
|
||||
let p = p.clone();
|
||||
s.edit(|d, _| {
|
||||
let cs = d.styles_mut().cell.iter_mut().find(|c| c.name == name).ok_or_else(|| bad("style.cell.edit", format!("no cell style `{name}`")))?;
|
||||
cell_style_fields(cs, &p, "style.cell.edit")?;
|
||||
reapply_cell_styles(d, std::slice::from_ref(&name));
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn table_style_fields(ts: &mut designcraft_doc::TableStyle, p: &Value) {
|
||||
for (k, f) in [
|
||||
("header", &mut ts.header),
|
||||
("body", &mut ts.body),
|
||||
("footer", &mut ts.footer),
|
||||
("leftColumn", &mut ts.left_column),
|
||||
("rightColumn", &mut ts.right_column),
|
||||
] {
|
||||
if let Some(v) = p.get(k) {
|
||||
*f = v.as_str().map(str::to_string);
|
||||
}
|
||||
}
|
||||
if let Some(b) = stroke_param(p.get("border")) {
|
||||
ts.border = Some(b);
|
||||
}
|
||||
if let Some(a) = p.get("altRows").and_then(Value::as_object) {
|
||||
let n = |k: &str, d: u32| a.get(k).and_then(Value::as_u64).map_or(d, |v| v as u32);
|
||||
let c = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or(designcraft_color::swatch::NONE).to_string();
|
||||
let t = |k: &str| a.get(k).and_then(Value::as_f64).map_or(1.0, |v| v.clamp(0.0, 1.0) as f32);
|
||||
ts.alt_rows = Some(designcraft_doc::AltFills {
|
||||
first: n("first", 1),
|
||||
first_color: c("firstColor"),
|
||||
first_tint: t("firstTint"),
|
||||
next: n("next", 1),
|
||||
next_color: c("nextColor"),
|
||||
next_tint: t("nextTint"),
|
||||
skip_first: n("skipFirst", 0),
|
||||
skip_last: n("skipLast", 0),
|
||||
});
|
||||
}
|
||||
if let Some(v) = p.get("spaceBefore").and_then(Value::as_f64) {
|
||||
ts.space_before = Some(v);
|
||||
}
|
||||
if let Some(v) = p.get("spaceAfter").and_then(Value::as_f64) {
|
||||
ts.space_after = Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
fn table_style_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base = str_param(p, "name").unwrap_or("Table Style 1").to_string();
|
||||
let mut ts = designcraft_doc::TableStyle::default();
|
||||
table_style_fields(&mut ts, p);
|
||||
s.edit(|d, _| {
|
||||
let name = designcraft_doc::Styles::unique_name(|n| d.styles.table.iter().any(|c| c.name == n), &base);
|
||||
ts.name = name.clone();
|
||||
d.styles_mut().table.push(ts);
|
||||
Ok(json!({"name": name}))
|
||||
})
|
||||
}
|
||||
|
||||
fn table_style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.table.apply", "missing name"))?.to_string();
|
||||
let doc = &s.doc()?.doc;
|
||||
let ts = doc.styles.table.iter().find(|c| c.name == name).cloned().ok_or_else(|| bad("style.table.apply", format!("no table style `{name}`")))?;
|
||||
let cells = doc.styles.cell.clone();
|
||||
let g = target(s, p, "style.table.apply")?;
|
||||
edit_table(s, &g, "style.table.apply", |t| {
|
||||
ts.apply_to(t, &cells);
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn table_style_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.table.edit", "missing name"))?.to_string();
|
||||
let p = p.clone();
|
||||
s.edit(|d, _| {
|
||||
let ts =
|
||||
d.styles_mut().table.iter_mut().find(|c| c.name == name).ok_or_else(|| bad("style.table.edit", format!("no table style `{name}`")))?;
|
||||
table_style_fields(ts, &p);
|
||||
let (ts, cells) = (ts.clone(), d.styles.cell.clone());
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
let Some(st) = d.story_mut(sid) else { continue };
|
||||
for t in st.tables.values_mut() {
|
||||
if t.style == name {
|
||||
ts.apply_to(std::sync::Arc::make_mut(t), &cells);
|
||||
}
|
||||
}
|
||||
st.rev += 1;
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod style_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn cell_and_table_styles_apply_and_follow_edits() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 500, 400], "content": "text", "text": ""})).unwrap();
|
||||
let sid = r["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap();
|
||||
s.execute("table.insert", &json!({"rows": 3, "cols": 2, "headerRows": 1})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Cell Head"})).unwrap();
|
||||
s.execute("style.cell.create", &json!({"name": "Head", "fill": "Black", "paragraphStyle": "Cell Head", "insets": 6})).unwrap();
|
||||
s.execute("style.cell.create", &json!({"name": "Body", "fill": "[Paper]"})).unwrap();
|
||||
s.execute("style.table.create", &json!({"name": "Data", "header": "Head", "body": "Body", "border": {"weight": 2, "color": "Black"}}))
|
||||
.unwrap();
|
||||
s.execute("style.table.apply", &json!({"name": "Data"})).unwrap();
|
||||
let t = |s: &Session| -> designcraft_doc::Table {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
(**d.story(designcraft_doc::StoryId(sid)).unwrap().tables.values().next().unwrap()).clone()
|
||||
};
|
||||
let tb = t(&s);
|
||||
assert_eq!(tb.style, "Data");
|
||||
assert_eq!(tb.options.border.weight, 2.0);
|
||||
let head = tb.cell(0, 0).unwrap();
|
||||
assert_eq!((head.fill.as_str(), head.style.as_str(), head.insets), ("Black", "Head", [6.0; 4]));
|
||||
assert_eq!(head.text.paras[0].style, "Cell Head");
|
||||
assert_eq!(tb.cell(1, 1).unwrap().style, "Body");
|
||||
// Editing the cell style updates its cells.
|
||||
s.execute("style.cell.edit", &json!({"name": "Head", "fill": "C=100 M=0 Y=0 K=0"})).unwrap();
|
||||
assert_eq!(t(&s).cell(0, 1).unwrap().fill, "C=100 M=0 Y=0 K=0");
|
||||
// Editing the table style re-applies it.
|
||||
s.execute("style.table.edit", &json!({"name": "Data", "border": {"weight": 4}})).unwrap();
|
||||
assert_eq!(t(&s).options.border.weight, 4.0);
|
||||
assert!(s.execute("style.cell.apply", &json!({"name": "Nope"})).is_err());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,33 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let run = |app: &mut DesignApp, id: &str, p: Value| {
|
||||
let _ = app.run(id, p);
|
||||
};
|
||||
// Table and cell styles: pick to apply, + to save the current look as a new style.
|
||||
if let Some(st) = app.session.active() {
|
||||
let styles = &st.doc.styles;
|
||||
let tnames: Vec<String> = styles.table.iter().map(|s| s.name.clone()).collect();
|
||||
let cnames: Vec<String> = styles.cell.iter().map(|s| s.name.clone()).collect();
|
||||
let tcur = info["style"].as_str().filter(|s| !s.is_empty()).unwrap_or(designcraft_doc::BASIC_TABLE).to_string();
|
||||
let ccur = cell["style"].as_str().filter(|s| !s.is_empty()).unwrap_or(designcraft_doc::NO_CELL_STYLE).to_string();
|
||||
for (label, kind, names, cur) in [("Table Style", "table", tnames, tcur), ("Cell Style", "cell", cnames, ccur)] {
|
||||
ui.horizontal(|ui| {
|
||||
caption(ui, label);
|
||||
egui::ComboBox::from_id_salt(("tp_style", kind)).selected_text(&cur).width(130.0).show_ui(ui, |ui| {
|
||||
for n in &names {
|
||||
if ui.selectable_label(*n == cur, n).clicked() {
|
||||
run(app, &format!("style.{kind}.apply"), json!({"name": n, "table": tid}));
|
||||
}
|
||||
}
|
||||
});
|
||||
if crate::icons::button(ui, "plus", 20.0, false, &format!("New {label}")).clicked() {
|
||||
if kind == "cell" {
|
||||
run(app, "style.cell.create", json!({"name": "Cell Style 1", "fromSelection": true}));
|
||||
} else {
|
||||
run(app, "style.table.create", json!({"name": "Table Style 1"}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
egui::Grid::new("table_panel").num_columns(4).spacing([6.0, 6.0]).show(ui, |ui| {
|
||||
caption(ui, "Rows");
|
||||
if let Some(v) = number(ui, "tp_rows", Some(rows.len() as f64), "", 52.0, 0) {
|
||||
|
||||
+5
-5
@@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 55 | 24 | 18 | 69% |
|
||||
| P1 | 97 | 57 | 24 | 16 | 71% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -25,8 +25,8 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Placing & links | 78% |
|
||||
| Type & text frames | 82% |
|
||||
| Typography | 83% |
|
||||
| Styles | 82% |
|
||||
| Tables | 73% |
|
||||
| Styles | 92% |
|
||||
| Tables | 81% |
|
||||
| Long documents | 58% |
|
||||
| Interactivity & digital | 30% |
|
||||
| Output & production | 58% |
|
||||
@@ -239,7 +239,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Styles | Paragraph styles | P0 | D |
|
||||
| Styles | Character styles | P0 | D |
|
||||
| Styles | Object styles | P1 | D |
|
||||
| Styles | Table & cell styles | P1 | M |
|
||||
| Styles | Table & cell styles | P1 | D |
|
||||
| Styles | Style groups | P1 | D |
|
||||
| Styles | Quick Apply | P1 | D |
|
||||
| Styles | Break link to style | P1 | D |
|
||||
@@ -253,7 +253,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Tables | Table options | P1 | D |
|
||||
| Tables | Cell options | P1 | D |
|
||||
| Tables | Graphic cells | P2 | M |
|
||||
| Tables | Table styles & cell styles | P1 | M |
|
||||
| Tables | Table styles & cell styles | P1 | D |
|
||||
| Tables | Table panel | P1 | D |
|
||||
| Tables | Overset cells | P1 | D |
|
||||
| Tables | Tables spanning frames | P1 | D |
|
||||
|
||||
Reference in new issue
Block a user