mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge pull request #1237 from aykutfirat/fix/cell-first-baseline
Tables: cell First Baseline offset (Cell Options › Text › First Baseline)
This commit is contained in:
@@ -125,7 +125,7 @@ fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, left
|
||||
let spec = FrameSpec {
|
||||
id: ItemId(0),
|
||||
area: Rect::new(0.0, 0.0, w.max(1.0), 1.0e6),
|
||||
opts: TextFrameOptions::default(),
|
||||
opts: TextFrameOptions { first_baseline: cell.first_baseline, first_baseline_min: cell.first_baseline_min, ..TextFrameOptions::default() },
|
||||
vertical: false,
|
||||
exclusions: vec![],
|
||||
outline: None,
|
||||
@@ -835,6 +835,23 @@ mod height_tests {
|
||||
AnchoredObject::new(designcraft_doc::Item::new(ItemId(0), LayerId(0), designcraft_doc::Shape::Rectangle, path), position)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_first_baseline_option_sets_the_first_line_and_the_row_height() {
|
||||
// Cell Options › Text › First Baseline: Cap Height sets the first line by the cap height,
|
||||
// not the typographic ascent, so a row sized for one line of caps keeps its height.
|
||||
let ascent = text_cell("Hd", 4.0);
|
||||
let mut cap = ascent.clone();
|
||||
cap.first_baseline = designcraft_doc::FirstBaseline::CapHeight;
|
||||
let (a, c) = (measure(&ascent, 92.0), measure(&cap, 92.0));
|
||||
let (la, lc) = (&a.text.frames[0].lines[0], &c.text.frames[0].lines[0]);
|
||||
assert!(lc.baseline < la.baseline, "cap height {} should sit above ascent {}", lc.baseline, la.baseline);
|
||||
assert!(c.content_h < a.content_h);
|
||||
let mut fixed = ascent.clone();
|
||||
fixed.first_baseline = designcraft_doc::FirstBaseline::Fixed;
|
||||
fixed.first_baseline_min = 6.0;
|
||||
assert!((measure(&fixed, 92.0).text.frames[0].lines[0].baseline - 6.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_bottom_inset_contains_descenders_without_growing_every_row() {
|
||||
let cell = text_cell("gyp", 8.5);
|
||||
|
||||
@@ -454,6 +454,11 @@ pub struct CellStyle {
|
||||
pub inset_overrides: [Option<f64>; 4],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vj: Option<crate::item::VerticalJustification>,
|
||||
/// Cell Options › Text › First Baseline, and its minimum.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first_baseline: Option<crate::item::FirstBaseline>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first_baseline_min: Option<f64>,
|
||||
/// All four edges.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stroke: Option<crate::table::CellStroke>,
|
||||
@@ -529,6 +534,12 @@ impl CellStyle {
|
||||
if let Some(v) = self.vj {
|
||||
cell.vj = v;
|
||||
}
|
||||
if let Some(v) = self.first_baseline {
|
||||
cell.first_baseline = v;
|
||||
}
|
||||
if let Some(v) = self.first_baseline_min {
|
||||
cell.first_baseline_min = v;
|
||||
}
|
||||
if let Some(v) = &self.stroke {
|
||||
cell.strokes = [v.clone(), v.clone(), v.clone(), v.clone()];
|
||||
cell.border_overrides = [true; 4];
|
||||
@@ -698,6 +709,12 @@ pub fn resolve_cell_style(cells: &[CellStyle], name: &str) -> CellStyle {
|
||||
if style.vj.is_some() {
|
||||
out.vj = style.vj;
|
||||
}
|
||||
if style.first_baseline.is_some() {
|
||||
out.first_baseline = style.first_baseline;
|
||||
}
|
||||
if style.first_baseline_min.is_some() {
|
||||
out.first_baseline_min = style.first_baseline_min;
|
||||
}
|
||||
if let Some(stroke) = &style.stroke {
|
||||
out.strokes = std::array::from_fn(|_| CellStrokeAttrs::from_stroke(stroke));
|
||||
}
|
||||
|
||||
+15
-1
@@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ids::StoryId;
|
||||
use crate::item::{StrokeType, VerticalJustification};
|
||||
use crate::item::{FirstBaseline, StrokeType, VerticalJustification};
|
||||
use crate::story::{CharFormat, ParaFormat, Story, TABLE_ANCHOR};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -159,6 +159,12 @@ fn full() -> f32 {
|
||||
fn no_fill() -> String {
|
||||
designcraft_color::swatch::NONE.into()
|
||||
}
|
||||
fn is_ascent(f: &FirstBaseline) -> bool {
|
||||
*f == FirstBaseline::Ascent
|
||||
}
|
||||
fn is_zero_min(v: &f64) -> bool {
|
||||
*v == 0.0
|
||||
}
|
||||
fn default_insets() -> [f64; 4] {
|
||||
[4.0; 4]
|
||||
}
|
||||
@@ -181,6 +187,12 @@ pub struct Cell {
|
||||
pub insets: [f64; 4],
|
||||
#[serde(default)]
|
||||
pub vj: VerticalJustification,
|
||||
/// Cell Options › Text › First Baseline: how far the first line's baseline sits below the
|
||||
/// top inset (IDML `FirstBaselineOffset`), and its minimum (`MinimumFirstBaselineOffset`).
|
||||
#[serde(default, skip_serializing_if = "is_ascent")]
|
||||
pub first_baseline: FirstBaseline,
|
||||
#[serde(default, skip_serializing_if = "is_zero_min")]
|
||||
pub first_baseline_min: f64,
|
||||
/// Text rotation in degrees (only 0 is composed for now).
|
||||
#[serde(default)]
|
||||
pub rotation: f64,
|
||||
@@ -216,6 +228,8 @@ impl Default for Cell {
|
||||
fill_tint: 1.0,
|
||||
insets: default_insets(),
|
||||
vj: VerticalJustification::Top,
|
||||
first_baseline: FirstBaseline::Ascent,
|
||||
first_baseline_min: 0.0,
|
||||
rotation: 0.0,
|
||||
strokes: Default::default(),
|
||||
border_overrides: [false; 4],
|
||||
|
||||
@@ -30,7 +30,7 @@ fn style_specs() -> Vec<CommandSpec> {
|
||||
"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?, type?, gapColor?, gapTint?} (all four edges; values as table.setCell's stroke), paragraphStyle?, basedOn?: cell style name, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} → {name}",
|
||||
"{name, fromSelection?: bool (the target cell's look), fill?, tint?, insets?: n|[t,l,b,r], vj?: top|center|bottom|justify, firstBaseline?: ascent|capHeight|leading|xHeight|fixed|emboxHeight, firstBaselineMin?: pt, stroke?: {weight?, color?, tint?, type?, gapColor?, gapTint?} (all four edges; values as table.setCell's stroke), paragraphStyle?, basedOn?: cell style name, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} → {name}",
|
||||
has_doc,
|
||||
|s, p| cell_style_create(s, p)
|
||||
),
|
||||
@@ -297,7 +297,7 @@ fn table_specs() -> Vec<CommandSpec> {
|
||||
"Cell Options",
|
||||
[],
|
||||
None,
|
||||
"{fill?: swatch, tint?: 0..1, insets?: n | [t,l,b,r], vj? | verticalJustification?: top|center|bottom|justify, text?, stroke?: {weight?, color?: swatch, tint?: 0..1, type?: solid|dashed|dotted|thickThin|thinThick|thinThin|thickThick|thinThickThin|thickThinThick|wavy|hashed | {kind:…}, gapColor?: swatch, gapTint?: 0..1, edges?: name | [names] (all|outer|inner|top|left|bottom|right|innerHorizontal|innerVertical; default all)}, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} — top/left/bottom/right are the target range's outer sides, innerHorizontal/innerVertical the lines between its rows/columns",
|
||||
"{fill?: swatch, tint?: 0..1, insets?: n | [t,l,b,r], vj? | verticalJustification?: top|center|bottom|justify, firstBaseline?: ascent|capHeight|leading|xHeight|fixed|emboxHeight, firstBaselineMin?: pt, text?, stroke?: {weight?, color?: swatch, tint?: 0..1, type?: solid|dashed|dotted|thickThin|thinThick|thinThin|thickThick|thinThickThin|thickThinThick|wavy|hashed | {kind:…}, gapColor?: swatch, gapTint?: 0..1, edges?: name | [names] (all|outer|inner|top|left|bottom|right|innerHorizontal|innerVertical; default all)}, story?, table?, rows?: [a, b] | row?, cols?: [a, b] | col? (the target cells; default: the selection)} — top/left/bottom/right are the target range's outer sides, innerHorizontal/innerVertical the lines between its rows/columns",
|
||||
in_table,
|
||||
set_cell
|
||||
),
|
||||
@@ -932,6 +932,22 @@ fn drawn_side(t: &Table, owners: &[(usize, usize)], (r, c): (usize, usize), side
|
||||
drawn.or_else(|| t.cell(r, c).and_then(|cell| cell.strokes.get(side).cloned()))
|
||||
}
|
||||
|
||||
/// Cell Options › Text › First Baseline: `firstBaseline` (offset method) and `firstBaselineMin` (pt).
|
||||
fn first_baseline_params(p: &Value, id: &str) -> Result<(Option<designcraft_doc::FirstBaseline>, Option<f64>)> {
|
||||
let fb = match p.get("firstBaseline").filter(|v| !v.is_null()) {
|
||||
Some(v) => Some(serde_json::from_value(v.clone()).map_err(|e| bad(id, format!("firstBaseline: {e}")))?),
|
||||
None => None,
|
||||
};
|
||||
let min = match p.get("firstBaselineMin").filter(|v| !v.is_null()) {
|
||||
Some(v) => match v.as_f64().filter(|m| m.is_finite() && (0.0..=8640.0).contains(m)) {
|
||||
Some(m) => Some(m),
|
||||
None => return Err(bad(id, "firstBaselineMin: a number of points from 0 to 8640")),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
Ok((fb, min))
|
||||
}
|
||||
|
||||
fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "table.setCell";
|
||||
let g = target(s, p, ID)?;
|
||||
@@ -954,6 +970,7 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(o) => return Err(bad(ID, format!("unknown vj `{o}`"))),
|
||||
None => None,
|
||||
};
|
||||
let (first_baseline, first_baseline_min) = first_baseline_params(p, ID)?;
|
||||
let text = str_param(p, "text").map(|_| super::text_param(p, "text"));
|
||||
let stroke = match p.get("stroke").filter(|v| !v.is_null()) {
|
||||
Some(sv) => Some((StrokeEdit::parse(doc, sv, ID)?, EdgeSel::parse(sv.get("edges"), ID)?)),
|
||||
@@ -998,6 +1015,12 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = vj {
|
||||
cell.vj = v;
|
||||
}
|
||||
if let Some(v) = first_baseline {
|
||||
cell.first_baseline = v;
|
||||
}
|
||||
if let Some(v) = first_baseline_min {
|
||||
cell.first_baseline_min = v;
|
||||
}
|
||||
if let Some(tx) = &text {
|
||||
let len = cell.text.len();
|
||||
cell.text.replace(0..len, tx);
|
||||
@@ -1525,7 +1548,7 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.filter_map(|(r, c)| {
|
||||
let cell = t.cell(r, c)?;
|
||||
Some(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, "style": cell.style}))
|
||||
"insets": cell.insets, "vj": cell.vj, "firstBaseline": cell.first_baseline, "firstBaselineMin": cell.first_baseline_min, "style": cell.style}))
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({
|
||||
@@ -1572,6 +1595,13 @@ fn cell_style_fields(cs: &mut designcraft_doc::CellStyle, p: &Value, stroke: Opt
|
||||
if let Some(v) = p.get("vj") {
|
||||
cs.vj = Some(serde_json::from_value(v.clone()).map_err(|e| bad(cmd, format!("vj: {e}")))?);
|
||||
}
|
||||
let (first_baseline, first_baseline_min) = first_baseline_params(p, cmd)?;
|
||||
if first_baseline.is_some() {
|
||||
cs.first_baseline = first_baseline;
|
||||
}
|
||||
if first_baseline_min.is_some() {
|
||||
cs.first_baseline_min = first_baseline_min;
|
||||
}
|
||||
if let Some(st) = stroke {
|
||||
cs.stroke = Some(st);
|
||||
cs.strokes = Default::default();
|
||||
@@ -1600,6 +1630,8 @@ fn cell_style_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
cs.fill_tint = Some(cell.fill_tint);
|
||||
cs.insets = Some(cell.insets);
|
||||
cs.vj = Some(cell.vj);
|
||||
cs.first_baseline = Some(cell.first_baseline);
|
||||
cs.first_baseline_min = Some(cell.first_baseline_min);
|
||||
cs.stroke = Some(cell.strokes[0].clone());
|
||||
cs.paragraph_style = cell.text.paras.first().map(|p| p.style.clone());
|
||||
}
|
||||
@@ -1787,6 +1819,28 @@ mod style_tests {
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn cell_first_baseline_is_a_cell_option_and_a_cell_style_attribute() {
|
||||
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": 2, "cols": 2})).unwrap();
|
||||
s.execute("table.setCell", &json!({"row": 0, "col": 0, "firstBaseline": "capHeight", "firstBaselineMin": 3})).unwrap();
|
||||
let got = s.execute("table.get", &json!({})).unwrap();
|
||||
let cell = got["cells"].as_array().unwrap().iter().find(|c| c["row"] == 0 && c["col"] == 0).unwrap().clone();
|
||||
assert_eq!((cell["firstBaseline"].as_str(), cell["firstBaselineMin"].as_f64()), (Some("capHeight"), Some(3.0)));
|
||||
assert!(s.execute("table.setCell", &json!({"row": 0, "col": 0, "firstBaseline": "middle"})).is_err());
|
||||
assert!(s.execute("table.setCell", &json!({"row": 0, "col": 0, "firstBaselineMin": -1})).is_err());
|
||||
s.execute("style.cell.create", &json!({"name": "Leading", "firstBaseline": "leading"})).unwrap();
|
||||
s.execute("style.cell.apply", &json!({"name": "Leading", "row": 1, "col": 1})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let t = d.story(designcraft_doc::StoryId(sid)).unwrap().tables.values().next().unwrap();
|
||||
assert_eq!(t.cell(1, 1).unwrap().first_baseline, designcraft_doc::FirstBaseline::Leading);
|
||||
assert_eq!(t.cell(1, 0).unwrap().first_baseline, designcraft_doc::FirstBaseline::Ascent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_and_table_styles_apply_and_follow_edits() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -923,6 +923,12 @@ impl<'a> Ex<'a> {
|
||||
if let Some(vj) = cs.vj {
|
||||
el.set("VerticalJustification", names::vj_out(vj));
|
||||
}
|
||||
if let Some(fb) = cs.first_baseline {
|
||||
el.set("FirstBaselineOffset", names::first_baseline_out(fb));
|
||||
}
|
||||
if let Some(min) = cs.first_baseline_min {
|
||||
el.set("MinimumFirstBaselineOffset", num(min));
|
||||
}
|
||||
if let Some(st) = &cs.stroke {
|
||||
for side in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"] {
|
||||
self.cell_stroke_attrs(&mut el, side, st);
|
||||
@@ -2770,6 +2776,8 @@ impl<'a> Ex<'a> {
|
||||
.attr("BottomInset", num(cell.insets[2]))
|
||||
.attr("RightInset", num(cell.insets[3]))
|
||||
.attr("VerticalJustification", vj)
|
||||
.attr("FirstBaselineOffset", names::first_baseline_out(cell.first_baseline))
|
||||
.attr("MinimumFirstBaselineOffset", num(cell.first_baseline_min))
|
||||
.attr("RotationAngle", num(cell.rotation));
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
// Do not turn absent native defaults into explicit shared-edge candidates.
|
||||
|
||||
@@ -1189,6 +1189,8 @@ impl<'r> Importer<'r> {
|
||||
cs.inset_overrides = [None; 4];
|
||||
}
|
||||
cs.vj = e.get("VerticalJustification").map(names::vj_in);
|
||||
cs.first_baseline = e.get("FirstBaselineOffset").map(names::first_baseline_in);
|
||||
cs.first_baseline_min = e.num("MinimumFirstBaselineOffset").filter(|v| v.is_finite()).map(|v| v.clamp(0.0, 8640.0));
|
||||
cs.strokes = ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].map(|side| self.cell_stroke_attrs(e, side));
|
||||
if let Some(stroke) = uniform_cell_stroke(&cs.strokes) {
|
||||
cs.stroke = Some(stroke);
|
||||
@@ -2213,6 +2215,12 @@ impl<'r> Importer<'r> {
|
||||
if let Some(vj) = ce.get("VerticalJustification") {
|
||||
cell.vj = names::vj_in(vj);
|
||||
}
|
||||
if let Some(fb) = ce.get("FirstBaselineOffset") {
|
||||
cell.first_baseline = names::first_baseline_in(fb);
|
||||
}
|
||||
if let Some(min) = ce.num("MinimumFirstBaselineOffset").filter(|v| v.is_finite()) {
|
||||
cell.first_baseline_min = min.clamp(0.0, 8640.0);
|
||||
}
|
||||
cell.rotation = ce.num("RotationAngle").unwrap_or(cell.rotation);
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
let attrs = self.cell_stroke_attrs(ce, side);
|
||||
|
||||
@@ -4193,3 +4193,44 @@ mod liquid_page_rules {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cell Options › Text › First Baseline, set by a cell style that a table style applies to its body
|
||||
/// region (cells with `[None]` take it from there) and overridden on one cell.
|
||||
const FIRST_BASELINE_STYLES: &str = r#"<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<RootCellStyleGroup>
|
||||
<CellStyle Self="CellStyle/$ID/[None]" Name="$ID/[None]"/>
|
||||
<CellStyle Self="cs-cap" Name="Cap" FirstBaselineOffset="CapHeight" MinimumFirstBaselineOffset="2" VerticalJustification="CenterAlign"/>
|
||||
<CellStyle Self="cs-cap-child" Name="CapChild" FillTint="50"><Properties><BasedOn type="object">cs-cap</BasedOn></Properties></CellStyle>
|
||||
</RootCellStyleGroup>
|
||||
<RootParagraphStyleGroup/>
|
||||
<RootTableStyleGroup>
|
||||
<TableStyle Self="ts" Name="Tbl" BodyRegionCellStyle="cs-cap-child"/>
|
||||
</RootTableStyleGroup></idPkg:Styles>"#;
|
||||
|
||||
const FIRST_BASELINE_STORY: &str = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<Story Self="s1"><ParagraphStyleRange><CharacterStyleRange><Table AppliedTableStyle="ts" HeaderRowCount="0">
|
||||
<Row Name="0" MinimumHeight="20"/>
|
||||
<Column Name="0" SingleColumnWidth="60"/><Column Name="1" SingleColumnWidth="60"/>
|
||||
<Cell Name="0:0" AppliedCellStyle="CellStyle/$ID/[None]"><ParagraphStyleRange><CharacterStyleRange><Content>Region</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
<Cell Name="1:0" AppliedCellStyle="CellStyle/$ID/[None]" FirstBaselineOffset="LeadingOffset"><ParagraphStyleRange><CharacterStyleRange><Content>Local</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
</Table></CharacterStyleRange></ParagraphStyleRange></Story></idPkg:Story>"#;
|
||||
|
||||
#[test]
|
||||
fn cell_first_baseline_comes_from_cell_styles_table_regions_and_cells_and_round_trips() {
|
||||
use designcraft_doc::FirstBaseline;
|
||||
let d = import_idml(&fixture_with_table_styles(FIRST_BASELINE_STYLES, FIRST_BASELINE_STORY)).unwrap();
|
||||
d.check().unwrap();
|
||||
let style = d.styles.cell.iter().find(|c| c.name == "Cap").unwrap();
|
||||
assert_eq!((style.first_baseline, style.first_baseline_min), (Some(FirstBaseline::CapHeight), Some(2.0)));
|
||||
let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
let region = table.cell(0, 0).unwrap();
|
||||
assert_eq!((region.first_baseline, region.first_baseline_min), (FirstBaseline::CapHeight, 2.0), "inherited through BasedOn and the body region");
|
||||
assert_eq!(table.cell(0, 1).unwrap().first_baseline, FirstBaseline::Leading, "a cell's own attribute wins");
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
let t2 = back.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(t2.cell(0, 0).unwrap().first_baseline, FirstBaseline::CapHeight);
|
||||
assert_eq!(t2.cell(0, 0).unwrap().first_baseline_min, 2.0);
|
||||
assert_eq!(t2.cell(0, 1).unwrap().first_baseline, FirstBaseline::Leading);
|
||||
let s2 = back.styles.cell.iter().find(|c| c.name == "Cap").unwrap();
|
||||
assert_eq!(s2.first_baseline, Some(FirstBaseline::CapHeight));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user