mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Part of #3229: setting type in a font or style that isn't available warns
Squashed from #4866 onto main.
This commit is contained in:
@@ -85,7 +85,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Create Text",
|
||||
[],
|
||||
None,
|
||||
"{x, y, text, rotation?: deg (about x, y), vertical?: bool = false, size?: pt, font?: family, style? (default Regular, or the family's closest style when it has no Regular), color?, keepModel?: bool (as paint.setFill), area?: {width, height, fit?: none|autoHeight|shrinkText, fitMinPercent?}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText), justify?: \"auto\" (default: the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\" (point type: aligned on x, so \"center\" centres each line on the point and \"right\" ends it there; text.setStyle changes it later), leadingModel?: \"romanBaseline\"|\"emBoxTop\", charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (default: emBoxTop and emBoxCenter while the interface is in Japanese, else romanBaseline)} → {id, bounds: {x, y, width, height} (the new type's box as document.inspect reports it, after justify), overflow?: [id] (area type whose text doesn't fit its frame: the lines past it aren't drawn; left out when it fits)}; new area type gets Auto Size (fit autoHeight) when the autoSizeAreaType preference is on",
|
||||
"{x, y, text, rotation?: deg (about x, y), vertical?: bool = false, size?: pt, font?: family, style? (default Regular, or the family's closest style when it has no Regular), color?, keepModel?: bool (as paint.setFill), area?: {width, height, fit?: none|autoHeight|shrinkText, fitMinPercent?}, placeholder?: bool (fill it with placeholder text instead, as the Type tools do with prefs placeholderText), justify?: \"auto\" (default: the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\" (point type: aligned on x, so \"center\" centres each line on the point and \"right\" ends it there; text.setStyle changes it later), leadingModel?: \"romanBaseline\"|\"emBoxTop\", charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (default: emBoxTop and emBoxCenter while the interface is in Japanese, else romanBaseline)} → {id, bounds: {x, y, width, height} (the new type's box as document.inspect reports it, after justify), overflow?: [id] (area type whose text doesn't fit its frame: the lines past it aren't drawn; left out when it fits), warning?: text (the font or style given isn't available: the type is drawn in the fallback font or the family's closest style)}; new area type gets Auto Size (fit autoHeight) when the autoSizeAreaType preference is on",
|
||||
has_doc,
|
||||
text_create
|
||||
),
|
||||
@@ -739,7 +739,17 @@ fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let made = insert_look_turned(s, "Type", NodeKind::Text(Box::new(t)), NewArt::plain(Appearance::default()), None, true, bbox_turn(rotation))?;
|
||||
let id = made.get("id").and_then(Value::as_u64).map(NodeId);
|
||||
let made = super::typecmd::with_bounds(s, id, made);
|
||||
Ok(super::typecmd::with_overflow(s, id.as_slice(), made))
|
||||
let mut made = super::typecmd::with_overflow(s, id.as_slice(), made);
|
||||
if p.get("font").is_some() || p.get("style").is_some() {
|
||||
let runs = match id.and_then(|i| s.doc().ok()?.doc.node(i)).map(|n| &n.kind) {
|
||||
Some(NodeKind::Text(t)) => t.runs.iter().map(|r| super::fonts::font_of(&r.style)).collect(),
|
||||
_ => Default::default(),
|
||||
};
|
||||
if let Some(w) = super::fonts::set_font_warning(runs) {
|
||||
made["warning"] = json!(w);
|
||||
}
|
||||
}
|
||||
Ok(made)
|
||||
}
|
||||
|
||||
/// The paragraph attributes new type gets (the Type tools, `text.create`): aligned to the start of
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Replace Font",
|
||||
[],
|
||||
None,
|
||||
"{from: {family, style?}, to: {family (any of its names; stored as the font's own family name), style?}, selectionOnly?} (style omitted: every style of the family / keep the closest style); text that is hidden or locked, in a hidden or locked group or layer, or on a locked artboard keeps its font → {runs, skipped?: the text objects left in the font, warning?}",
|
||||
"{from: {family, style?}, to: {family (any of its names; stored as the font's own family name), style?}, selectionOnly?} (style omitted: every style of the family / keep the closest style); text that is hidden or locked, in a hidden or locked group or layer, or on a locked artboard keeps its font → {runs, skipped?: the text objects left in the font, warning?: text (some text was left in its font, or the style set isn't available: its type is drawn in the family's closest style)}",
|
||||
has_doc,
|
||||
replace
|
||||
),
|
||||
@@ -318,6 +318,24 @@ pub(super) fn substitution_warning(d: &Document) -> Option<String> {
|
||||
|
||||
/// [`substitution_warning`] for type set in `fonts`.
|
||||
pub(super) fn substitution_warning_of(fonts: BTreeSet<UsedFont>) -> Option<String> {
|
||||
substitutions(fonts, ("were written", "was drawn"))
|
||||
}
|
||||
|
||||
/// The font (family, style, version) of character style `st`.
|
||||
pub(super) fn font_of(st: &vectorcraft_doc::CharStyle) -> UsedFont {
|
||||
(st.font_family.clone(), st.font_style.clone(), st.font_version.clone())
|
||||
}
|
||||
|
||||
/// What `text.create`, `text.setStyle`, `text.setRangeStyle` and `text.replaceFont` say when the
|
||||
/// fonts they set type in (`fonts`) don't draw it: the family isn't available (the fallback font
|
||||
/// is drawn) or doesn't have that style (its closest style is drawn). `None` when all draw it.
|
||||
pub(super) fn set_font_warning(fonts: BTreeSet<UsedFont>) -> Option<String> {
|
||||
substitutions(fonts, ("are drawn", "is drawn"))
|
||||
}
|
||||
|
||||
/// The warning for type set in `fonts`, with the verbs `written` and `drawn` (an export's are
|
||||
/// `were written` and `was drawn`).
|
||||
fn substitutions(fonts: BTreeSet<UsedFont>, (written, drawn): (&str, &str)) -> Option<String> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let mut missing: Vec<String> = Vec::new();
|
||||
let mut styles: BTreeSet<String> = BTreeSet::new();
|
||||
@@ -325,7 +343,7 @@ pub(super) fn substitution_warning_of(fonts: BTreeSet<UsedFont>) -> Option<Strin
|
||||
match db.resolve(&family, &style) {
|
||||
None | Some((_, vectorcraft_text::FontMatch::Missing)) => missing.push(family),
|
||||
Some((face, vectorcraft_text::FontMatch::Style)) => {
|
||||
styles.insert(format!("{family} {style} was drawn in {} {}", face.family, face.style));
|
||||
styles.insert(format!("{family} {style} {drawn} in {} {}", face.family, face.style));
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
@@ -335,14 +353,14 @@ pub(super) fn substitution_warning_of(fonts: BTreeSet<UsedFont>) -> Option<Strin
|
||||
let mut parts = Vec::new();
|
||||
if !missing.is_empty() {
|
||||
parts.push(format!(
|
||||
"fonts that aren't available were written in the fallback font, {}: {} (install them, or replace them with Find Font)",
|
||||
"fonts that aren't available {written} in the fallback font, {}: {} (install them, or replace them with Find Font)",
|
||||
vectorcraft_text::FALLBACK_FAMILY,
|
||||
missing.join(", ")
|
||||
));
|
||||
}
|
||||
if !styles.is_empty() {
|
||||
parts.push(format!(
|
||||
"font styles that aren't available were written in the closest style: {} (pick a style the family has)",
|
||||
"font styles that aren't available {written} in the closest style: {} (pick a style the family has)",
|
||||
styles.into_iter().collect::<Vec<_>>().join(", ")
|
||||
));
|
||||
}
|
||||
@@ -426,6 +444,8 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let skipped = other.into_iter().filter(|id| text(d, *id).is_some_and(|t| t.runs.iter().any(|r| matches(&r.style)))).count();
|
||||
(ids, skipped)
|
||||
};
|
||||
// The fonts the replaced runs are set in, for the warning.
|
||||
let mut set = BTreeSet::new();
|
||||
let n = s.edit("Replace Font", |d, _| {
|
||||
let mut n = 0;
|
||||
for id in &ids {
|
||||
@@ -443,6 +463,7 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Another font: none of the old one's versions or Variable Font settings.
|
||||
r.style.font_version = None;
|
||||
r.style.font_axes.clear();
|
||||
set.insert(font_of(&r.style));
|
||||
changed = true;
|
||||
n += 1;
|
||||
}
|
||||
@@ -455,9 +476,14 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(n)
|
||||
})?;
|
||||
let mut out = json!({ "runs": n });
|
||||
let mut warnings = Vec::new();
|
||||
if skipped > 0 {
|
||||
out["skipped"] = json!(skipped);
|
||||
out["warning"] = json!(REPLACE_SKIPPED);
|
||||
warnings.push(REPLACE_SKIPPED.to_string());
|
||||
}
|
||||
warnings.extend(set_font_warning(set));
|
||||
if !warnings.is_empty() {
|
||||
out["warning"] = json!(warnings.join("; "));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -751,6 +777,48 @@ mod open_tests {
|
||||
assert!(warnings.contains("Source Sans 3 Black Wide was drawn in Source Sans 3"), "{out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_type_in_a_font_it_cant_draw_warns() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let warning = |out: &Value| out["warning"].as_str().map(str::to_string);
|
||||
let made = s.execute("text.create", &json!({"x": 10, "y": 20, "text": "Hi", "font": "Source Sans 3", "style": "Black Wide"})).unwrap();
|
||||
assert!(warning(&made).is_some_and(|w| w.contains("Source Sans 3 Black Wide is drawn in Source Sans 3")), "{made}");
|
||||
let gone = s.execute("text.create", &json!({"x": 10, "y": 60, "text": "Gone", "font": "No Such Font Family"})).unwrap();
|
||||
let w = warning(&gone).unwrap_or_default();
|
||||
assert!(w.contains("No Such Font Family") && w.contains(vectorcraft_text::FALLBACK_FAMILY), "{gone}");
|
||||
let plain = s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Fine", "font": "Source Sans 3", "style": "Semibold"})).unwrap();
|
||||
assert_eq!(warning(&plain), None, "{plain}");
|
||||
let id = plain["id"].as_u64().unwrap();
|
||||
let set = s.execute("text.setStyle", &json!({"ids": [id], "style": "Black Wide"})).unwrap();
|
||||
assert!(warning(&set).is_some_and(|w| w.contains("Source Sans 3 Black Wide is drawn in Source Sans 3")), "{set}");
|
||||
let back = s.execute("text.setStyle", &json!({"ids": [id], "style": "Bold"})).unwrap();
|
||||
assert_eq!(warning(&back), None, "{back}");
|
||||
// Only the font the command set: the other objects' substitutes aren't its to report.
|
||||
let size = s.execute("text.setStyle", &json!({"ids": [id], "size": 30})).unwrap();
|
||||
assert_eq!(warning(&size), None, "{size}");
|
||||
// Only the characters the command styles: with "Fi" in Black Wide, styling "ne" doesn't warn.
|
||||
let first = s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 2, "style": "Black Wide"})).unwrap();
|
||||
assert!(warning(&first).is_some_and(|w| w.contains("Source Sans 3 Black Wide is drawn in Source Sans 3")), "{first}");
|
||||
let rest = s.execute("text.setRangeStyle", &json!({"id": id, "start": 2, "end": 4, "font": "Source Sans 3"})).unwrap();
|
||||
assert_eq!(warning(&rest), None, "{rest}");
|
||||
let range = s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 2, "font": "No Such Font Family"})).unwrap();
|
||||
assert!(warning(&range).is_some_and(|w| w.contains("No Such Font Family")), "{range}");
|
||||
let replaced = s
|
||||
.execute(
|
||||
"text.replaceFont",
|
||||
&json!({"from": {"family": "No Such Font Family"}, "to": {"family": "Source Sans 3", "style": "Black Wide"}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(warning(&replaced).is_some_and(|w| w.contains("Source Sans 3 Black Wide is drawn in Source Sans 3")), "{replaced}");
|
||||
// Empty type: the font set is the one the next characters typed get.
|
||||
let empty = s.execute("text.create", &json!({"x": 10, "y": 140, "text": ""})).unwrap()["id"].as_u64().unwrap();
|
||||
let set = s.execute("text.setStyle", &json!({"ids": [empty], "style": "Black Wide"})).unwrap();
|
||||
assert!(warning(&set).is_some_and(|w| w.contains("Black Wide")), "{set}");
|
||||
let range = s.execute("text.setRangeStyle", &json!({"id": empty, "font": "No Such Font Family"})).unwrap();
|
||||
assert!(warning(&range).is_some_and(|w| w.contains("No Such Font Family")), "{range}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opened_documents_have_exact_text_bounds() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs}",
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -708,6 +708,9 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let t = text_ref(s, id)?;
|
||||
change.localize(&t, C)?;
|
||||
let (a, b) = range_of(p, edit::runs_len(&t.runs), C)?;
|
||||
// The fonts the styled characters are set in, for the warning.
|
||||
let mut set = std::collections::BTreeSet::new();
|
||||
let sets_font = change.font.is_some() || change.style.is_some();
|
||||
let n = s.edit("Character", |d, _| {
|
||||
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
|
||||
if a == b && !t.plain_text().is_empty() {
|
||||
@@ -719,10 +722,19 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(before) = before {
|
||||
protect_missing_glyphs(&before, &mut t.runs);
|
||||
}
|
||||
if sets_font {
|
||||
// Empty text has one run, which the empty range styles.
|
||||
let styled = if a == b { t.runs.clone() } else { edit::slice_runs(&t.runs, a, b) };
|
||||
set.extend(styled.iter().map(|r| super::fonts::font_of(&r.style)));
|
||||
}
|
||||
refresh_bounds(t);
|
||||
Ok(t.runs.len())
|
||||
})?;
|
||||
Ok(json!({"id": id.0, "runs": n}))
|
||||
let mut out = json!({"id": id.0, "runs": n});
|
||||
if let Some(w) = super::fonts::set_font_warning(set) {
|
||||
out["warning"] = json!(w);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The em a laid-out glyph's kerning and tracking are measured in, in text space: the size it
|
||||
|
||||
@@ -123,7 +123,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, font?, style? (font alone: the style is kept when the new family has it, else its closest), size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"auto\" (the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour, keepModel?: bool (as paint.setFill), features?: [\"dlig\", \"-liga\", …] OpenType, start?: byte, end?: byte} (size/leading are document points after object scaling; with a range: the character attributes style that range and justify the paragraphs it touches; without: all the text) → {ids, overflow?: [id…] (area type and type on a path whose text no longer fits; left out when all fits)}",
|
||||
"{ids?|id?, font?, style? (font alone: the style is kept when the new family has it, else its closest), size?: pt, leading?: pt|\"auto\", tracking?: 1/1000 em, justify?: \"auto\" (the start of each paragraph's direction)|\"left\"|\"center\"|\"right\"|\"justifyAll\", fill?: colour, keepModel?: bool (as paint.setFill), features?: [\"dlig\", \"-liga\", …] OpenType, start?: byte, end?: byte} (size/leading are document points after object scaling; with a range: the character attributes style that range and justify the paragraphs it touches; without: all the text) → {ids, overflow?: [id…] (area type and type on a path whose text no longer fits; left out when all fits), warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
has_doc,
|
||||
set_style
|
||||
),
|
||||
@@ -805,6 +805,8 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let range = TextRange::parse(p, C)?;
|
||||
let ids = text_targets(s, p, C)?;
|
||||
let protect = s.prefs.missing_glyph_protection && (font.is_some() || style.is_some());
|
||||
// The fonts the styled characters are set in, for the warning.
|
||||
let mut set = std::collections::BTreeSet::new();
|
||||
s.edit("Character", |d, _| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
@@ -846,6 +848,17 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(before) = before {
|
||||
super::textedit::protect_missing_glyphs(&before, &mut t.runs);
|
||||
}
|
||||
if font.is_some() || style.is_some() {
|
||||
// All the text: every run, the one run of empty text included.
|
||||
let styled = match range {
|
||||
None => t.runs.clone(),
|
||||
Some(r) => {
|
||||
let (a, b) = r.bytes(t);
|
||||
vectorcraft_text::edit::slice_runs(&t.runs, a, b)
|
||||
}
|
||||
};
|
||||
set.extend(styled.iter().map(|r| super::fonts::font_of(&r.style)));
|
||||
}
|
||||
if let Some(j) = justify {
|
||||
let span = para_span(range, t);
|
||||
t.edit_paras(span, |pa| pa.justify = j);
|
||||
@@ -888,7 +901,11 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(with_overflow(s, &ids, json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })))
|
||||
let mut out = with_overflow(s, &ids, json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }));
|
||||
if let Some(w) = super::fonts::set_font_warning(set) {
|
||||
out["warning"] = json!(w);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Reshape area type's frame with `f` ([`TextObject::transform_area`],
|
||||
|
||||
Reference in New Issue
Block a user