Files
wordcraft/crates/layout/src/vertical.rs
T
50acc0c5f3 Vertical text for sections (w:textDirection) (#491) (#496)
- Model: SectionProps.text_direction (horizontal, tbRl, btLr); unknown
  values read as horizontal.
- DOCX: w:textDirection in w:sectPr read and written in CT_SectPr order
  (after w:titlePg, before w:bidi), also inside w:sectPrChange.
- Layout: a vertical section is laid out on its page turned on its side
  (margins, page borders and columns turned with it) and each page is
  turned onto the real page: tbRl lines run top to bottom stacking from
  the right, btLr bottom to top from the left. Pictures, shapes and text
  boxes stay upright; headers and footers stay horizontal; a section
  turning the other way starts a new page.
- Display: in top-to-bottom text ideographs, kana, Hangul and full-width
  forms stand upright (ideographic comma/full stop moved to the upper
  right); Latin lies on its side. Raster and PDF draw it unchanged.
- Hit testing: clicks outside turned lines find the nearest turned line.
- Command layout.textDirection (Layout › Page Setup › Text Direction):
  turns the selected cells or the text box the caret is in, else the
  section; ribbon menu and keytip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:45:49 -04:00

124 lines
5.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.
//! Vertical sections (Layout › Text Direction, `w:textDirection` in `w:sectPr`).
//!
//! A vertical section is laid out unturned on a page turned onto its side — lines as long as the
//! page's text area is tall, stacking across its width — and each finished page is then turned
//! onto the real page, as a table cell's turned text is: top-to-bottom lines stack from the right
//! (`tbRl`), bottom-to-top ones from the left (`btLr`). Pictures, shapes and text boxes keep
//! standing upright where the turn carries them; headers and footers stay horizontal.
use wordcraft_doc::StoryRef;
use wordcraft_doc::props::{Borders, TextDirection};
use wordcraft_doc::section::SectionProps;
use wordcraft_geom::{Point, Rect};
use crate::{Page, Placed, turn_point, turn_rect};
/// The page a section running `turn` is laid out on, unturned: the real page `s` on its side,
/// its margins turned with it. `body_top` is where the real page's body starts (below a tall
/// header). Columns divide the turned page's width, so they stack down the real page.
pub(crate) fn unturned_sect(s: &SectionProps, body_top: f32) -> SectionProps {
let mut v = s.clone();
v.page_w = s.page_h;
v.page_h = s.page_w;
v.gutter = 0.0;
let (top, right, bottom, left) = (body_top, s.margin_right, s.margin_bottom, s.margin_left + s.gutter);
// The unturned frame's (u, v) lands at (W - v, u) for `Down` and (v, H - u) for `Up`: its
// top edge is the real right edge (left for `Up`), its left edge the real top (bottom).
match s.text_direction {
TextDirection::Horizontal => return v,
TextDirection::Down => {
(v.margin_top, v.margin_left, v.margin_bottom, v.margin_right) = (right, top, left, bottom);
v.page_borders = s.page_borders.as_ref().map(|b| Borders { top: b.right, left: b.top, bottom: b.left, right: b.bottom, ..*b });
}
TextDirection::Up => {
(v.margin_top, v.margin_left, v.margin_bottom, v.margin_right) = (left, bottom, right, top);
v.page_borders = s.page_borders.as_ref().map(|b| Borders { top: b.left, left: b.bottom, bottom: b.right, right: b.top, ..*b });
}
}
v
}
/// Where the unturned frame's origin lands on the real `w` × `h` page.
fn origin(turn: TextDirection, w: f32, h: f32) -> (f32, f32) {
match turn {
TextDirection::Horizontal => (0.0, 0.0),
TextDirection::Down => (w, 0.0),
TextDirection::Up => (0.0, h),
}
}
/// Turn a page laid out unturned (see [`unturned_sect`]) onto the real `phys` page.
pub(crate) fn turn_page(page: &mut Page, turn: TextDirection, phys: &SectionProps) {
if !turn.is_turned() {
return;
}
let (ox, oy) = origin(turn, phys.page_w, phys.page_h);
page.w = phys.page_w;
page.h = phys.page_h;
page.body = turn_rect(turn, ox, oy, page.body);
// Pictures, shapes and text boxes stand upright: their centres move with the turn. A text
// box's text moves with its box (it runs as the box says, not as the page does).
let upright = |r: Rect| {
let (cx, cy) = turn_point(turn, ox, oy, r.x + r.w / 2.0, r.y + r.h / 2.0);
Rect::new(cx - r.w / 2.0, cy - r.h / 2.0, r.w, r.h)
};
let mut boxes: Vec<(u32, Rect, (f32, f32))> = Vec::new();
for it in &mut page.items {
if let Placed::Object { rect, origin, text_box, .. } = it {
let r = upright(*rect);
if let Some(id) = text_box {
boxes.push((*id, *rect, (r.x - rect.x, r.y - rect.y)));
}
*rect = r;
(origin.x, origin.y) = turn_point(turn, ox, oy, origin.x, origin.y);
}
}
let in_box = |it: &Placed| -> Option<(f32, f32)> {
if let Placed::Lines { story: StoryRef::Part(id), .. } = it
&& let Some((_, _, d)) = boxes.iter().find(|(b, _, _)| b == id)
{
return Some(*d);
}
let r = bounds(it)?;
boxes
.iter()
.find(|(_, b, _)| b.expand(0.5).contains(Point::new(r.x, r.y)) && b.expand(0.5).contains(Point::new(r.right(), r.bottom())))
.map(|(_, _, d)| *d)
};
for it in &mut page.items {
if matches!(it, Placed::Object { .. }) {
continue;
}
if let Some((dx, dy)) = in_box(it) {
it.translate(dx, dy);
continue;
}
match it {
Placed::Image { rect, .. } | Placed::Shape { rect, .. } | Placed::Graphic { rect, .. } => *rect = upright(*rect),
_ => it.turn(turn, ox, oy),
}
}
}
/// An item's unturned page area (lines: their band; `None` for turned lines, which a text box
/// in the section holds).
fn bounds(it: &Placed) -> Option<Rect> {
match it {
Placed::Lines { para, l0, l1, x, y, turn, .. } if !turn.is_turned() => {
let first = para.lines.get(*l0)?;
let last = para.lines.get(l1.checked_sub(1)?)?;
let right = para.lines.get(*l0..*l1).unwrap_or(&[]).iter().map(|l| l.right).fold(0.0f32, f32::max);
Some(Rect::new(*x, *y, right, last.top + last.height - first.top))
}
Placed::Lines { .. } => it.turned_bounds(),
Placed::Fill { rect, .. }
| Placed::Image { rect, .. }
| Placed::Shape { rect, .. }
| Placed::Graphic { rect, .. }
| Placed::Clip { rect, .. }
| Placed::Cell { rect, .. }
| Placed::Object { rect, .. } => Some(*rect),
Placed::Rule { x0, y0, x1, y1, .. } => Some(Rect::new(x0.min(*x1), y0.min(*y1), (x1 - x0).abs(), (y1 - y0).abs())),
}
}