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:
Brandon Thomas
2026-10-02 16:40:46 -04:00
co-authored by Claude Opus 5.5
parent 08727dfd7e
commit e433c77b55
7 changed files with 488 additions and 8 deletions
+1
View File
@@ -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.
+121
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -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),
+317 -2
View File
@@ -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());
}
}
+27
View File
@@ -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
View File
@@ -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 |