Files
vectorcraft/crates/doc/src/text/area.rs
T
dotCooCoo f46427167c .ai import: area type keeps its rows, columns, gutters and first baseline
Area type made from the editing data's text document came in with one
row, one column, the 18 pt gutter and the Ascent first baseline,
whatever its frame kept. Each area frame now keeps its rows and columns,
the gutter between rows and the gutter between columns, and its first
baseline (Fixed, Cap Height, Leading or x Height) with its minimum,
scaled with the art's units. Each frame of a thread keeps its own, and a
key that is missing or can't be read leaves that option as it was. A
first baseline the frame keeps is no longer replaced by the fixed one
that matches the file's first line.

AreaOptions gets row_gutter (rowGutter, left out while unset), which
layout spaces rows by; text.areaOptions takes and reports rowGutter, a
gutter given alone sets both, and the Area Type Options dialog shows
Row Gutter.
2026-11-02 12:04:07 +07:00

251 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::para_style::FirstBaseline;
use super::{Deserialize, PathData, Serialize};
/// Area Type Options "Align" (vertical): where the lines of each row/column sit in it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VerticalAlign {
/// Lines start at the top of the cell (the default).
#[default]
Top,
/// The block of lines is centred in the cell.
Center,
/// The last line's descent touches the cell bottom.
Bottom,
/// The first line stays at the top, the last one moves to the bottom and the space left over
/// is shared equally between the lines (no paragraph spacing limit).
Justify,
}
impl VerticalAlign {
pub const ALL: [VerticalAlign; 4] = [VerticalAlign::Top, VerticalAlign::Center, VerticalAlign::Bottom, VerticalAlign::Justify];
pub fn id(self) -> &'static str {
match self {
VerticalAlign::Top => "top",
VerticalAlign::Center => "center",
VerticalAlign::Bottom => "bottom",
VerticalAlign::Justify => "justify",
}
}
}
/// Area Type Options › Text Flow: the order text fills the cells of area type with several rows
/// and columns.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TextFlow {
/// Down each column's rows, then on to the next column (the default, and what a document
/// from before the option reads as).
#[default]
Columns,
/// Across each row's columns, then on to the next row.
Rows,
}
/// Text Wrap Options of a wrap object (Object → Text Wrap).
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct TextWrap {
/// Distance kept between the text and the object, in points (Illustrator's default 6 pt).
pub offset: f64,
/// Invert Wrap: text flows inside the object instead of around it.
pub invert: bool,
}
impl Default for TextWrap {
fn default() -> Self {
Self { offset: 6.0, invert: false }
}
}
/// A resolved wrap shape on an area text object: the outline of a wrap object in text space.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WrapShape {
pub path: PathData,
#[serde(flatten)]
pub wrap: TextWrap,
}
/// Type on a Path effect: how each glyph is oriented on the path (Type → Type on a Path).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PathEffect {
/// Glyphs rotate with the path (the default).
#[default]
Rainbow,
/// Vertical edges stay vertical; the baseline follows the path.
Skew,
/// Horizontal edges stay horizontal; vertical edges are perpendicular to the path.
#[serde(rename = "3dRibbon")]
Ribbon3d,
/// No rotation: the left end of each glyph's baseline sits on the path.
StairStep,
/// The baseline centre sits on the path and glyphs point away from the path's centre.
Gravity,
}
impl PathEffect {
pub const ALL: [PathEffect; 5] = [PathEffect::Rainbow, PathEffect::Skew, PathEffect::Ribbon3d, PathEffect::StairStep, PathEffect::Gravity];
pub fn id(self) -> &'static str {
match self {
PathEffect::Rainbow => "rainbow",
PathEffect::Skew => "skew",
PathEffect::Ribbon3d => "3dRibbon",
PathEffect::StairStep => "stairStep",
PathEffect::Gravity => "gravity",
}
}
pub fn parse(s: &str) -> Option<Self> {
let k = s.to_ascii_lowercase().replace([' ', '-', '_'], "");
Self::ALL.into_iter().find(|e| e.id().to_ascii_lowercase() == k || (k == "ribbon3d" && *e == PathEffect::Ribbon3d))
}
}
/// Type on a Path Options › Align to Path: which height of the type runs along the path.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PathAlign {
/// The font's top edge: the type hangs below the path.
Ascender,
/// The font's bottom edge: the type stands above the path.
Descender,
/// Halfway between the ascender and the descender.
Center,
/// The baseline (the default).
#[default]
Baseline,
}
impl PathAlign {
pub const ALL: [PathAlign; 4] = [PathAlign::Ascender, PathAlign::Descender, PathAlign::Center, PathAlign::Baseline];
pub fn id(self) -> &'static str {
match self {
PathAlign::Ascender => "ascender",
PathAlign::Descender => "descender",
PathAlign::Center => "center",
PathAlign::Baseline => "baseline",
}
}
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|a| a.id().eq_ignore_ascii_case(s.trim()))
}
}
/// Area Type Options: rows and columns, gutters, inset and first baseline of area type.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AreaOptions {
#[serde(deserialize_with = "de_rows_or_columns")]
pub rows: usize,
#[serde(deserialize_with = "de_rows_or_columns")]
pub columns: usize,
/// Gutter between rows/columns in points.
pub gutter: f64,
/// Gutter between rows in points when it differs from [`Self::gutter`], which is then the
/// gutter between columns (left out when saved while unset).
#[serde(skip_serializing_if = "Option::is_none")]
pub row_gutter: Option<f64>,
/// Inset from the frame edges in points.
pub inset: f64,
pub first_baseline: FirstBaseline,
/// Minimum first-baseline offset in points.
pub first_baseline_min: f64,
/// Vertical alignment of the lines in each row/column.
pub vertical_align: VerticalAlign,
/// Text Flow: by columns (the default, left out when saved) or by rows.
#[serde(skip_serializing_if = "crate::skip::is_default")]
pub flow: TextFlow,
/// How the frame and its text fit each other (Auto Size, Shrink Text to Fit).
#[serde(skip_serializing_if = "crate::skip::is_default")]
pub fit: AreaFit,
}
impl Default for AreaOptions {
fn default() -> Self {
Self {
rows: 1,
columns: 1,
gutter: 18.0,
row_gutter: None,
inset: 0.0,
first_baseline: FirstBaseline::Ascent,
first_baseline_min: 0.0,
vertical_align: VerticalAlign::Top,
flow: TextFlow::Columns,
fit: AreaFit::None,
}
}
}
impl AreaOptions {
/// The most rows, and the most columns, area type can have.
pub const MAX_ROWS_COLUMNS: usize = 100;
}
/// [`AreaOptions::rows`] or [`AreaOptions::columns`], read as a `u64` and clamped to
/// 1..=[`AreaOptions::MAX_ROWS_COLUMNS`].
fn de_rows_or_columns<'de, D: serde::Deserializer<'de>>(d: D) -> Result<usize, D::Error> {
let n = usize::try_from(u64::deserialize(d)?).unwrap_or(usize::MAX);
Ok(n.clamp(1, AreaOptions::MAX_ROWS_COLUMNS))
}
/// How area type and its frame fit each other. Serialized as `"none"`, `"autoHeight"` or
/// `{"shrinkText": {"minPercent": 50}}`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AreaFit {
/// The frame keeps its size; text that doesn't fit overflows.
#[default]
None,
/// The frame's height follows the text (Illustrator's Auto Size): resolved by the engine after
/// each edit, for rectangular frames of horizontal type in one row.
AutoHeight,
/// Text that overflows is scaled down (size, leading and baseline shift; not paragraph
/// spacing) by the largest factor down to `min_percent` % that makes it fit, at layout time.
ShrinkText {
#[serde(rename = "minPercent", default = "AreaFit::default_min_percent")]
min_percent: f64,
},
}
impl AreaFit {
/// The smallest Shrink Text to Fit percentage allowed.
pub const MIN_PERCENT: f64 = 10.0;
/// The Shrink Text to Fit percentage new settings start with.
pub const DEFAULT_MIN_PERCENT: f64 = 50.0;
fn default_min_percent() -> f64 {
Self::DEFAULT_MIN_PERCENT
}
/// Its id: `none`, `autoHeight` or `shrinkText`.
pub fn id(self) -> &'static str {
match self {
AreaFit::None => "none",
AreaFit::AutoHeight => "autoHeight",
AreaFit::ShrinkText { .. } => "shrinkText",
}
}
/// The fit an id names (case, spaces, dashes and underscores ignored), with `min_percent`
/// for Shrink Text (clamped to 10..100; a non-finite value gives the default).
pub fn parse(id: &str, min_percent: Option<f64>) -> Option<Self> {
match id.to_ascii_lowercase().replace([' ', '-', '_'], "").as_str() {
"none" | "off" => Some(AreaFit::None),
"autoheight" | "autosize" => Some(AreaFit::AutoHeight),
"shrinktext" | "shrinktexttofit" | "shrink" => {
Some(AreaFit::ShrinkText { min_percent: Self::clamp_percent(min_percent.unwrap_or(Self::DEFAULT_MIN_PERCENT)) })
}
_ => None,
}
}
/// A Shrink Text minimum percentage within 10..100 (the default when not finite).
pub fn clamp_percent(p: f64) -> f64 {
if p.is_finite() { p.clamp(Self::MIN_PERCENT, 100.0) } else { Self::DEFAULT_MIN_PERCENT }
}
/// Shrink Text's minimum scale factor (0.1..1), if this is Shrink Text.
pub fn min_scale(self) -> Option<f64> {
match self {
AreaFit::ShrinkText { min_percent } => Some(Self::clamp_percent(min_percent) / 100.0),
_ => None,
}
}
}