T: right-to-left carets and selections; Binding: Right to Left

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-04 01:34:47 -04:00
parent 6334734b3a
commit 5b7969901c
13 changed files with 244 additions and 61 deletions

View File

@ -96,7 +96,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Split Window / New Window (`window.split`, `window.newWindow`, Window ▸ Arrange): two side-by-side views of the document with their own zoom, scroll and render cache. New Window puts the second view in its own OS window (an egui viewport; a floating window on the web).
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. No in-app playback or PDF rich media.
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Caret movement in visual order isn't there yet.
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel for right-to-left text; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
- Interface language (Edit ▸ Interface Language, `app.language`): German, French, Spanish and Japanese menu titles, common menu items and panel names (our own translations); dialogs and the rest of the UI stay English.
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with our own Generic CMYK ICC profile (lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.

View File

@ -103,6 +103,8 @@ pub struct PlacedGlyph {
pub upright: bool,
/// Tate-chu-yoko placement (see [`shape::Glyph::tcy`]).
pub tcy: Option<[f64; 3]>,
/// Set right to left (an odd bidi level): the caret before it is at its right edge.
pub rtl: bool,
}
impl PlacedGlyph {
@ -1587,6 +1589,10 @@ fn layout_line(
let leader = leaders.iter().find(|(k, _)| *k == i).map(|(_, l)| l.as_str());
let (tab_x, tab_w) = (x, p.adv);
x += p.adv;
if let Some((_, l)) = kashidas.iter().find(|k| k.0 == i) {
// The glyph keeps its own width; the gap before it is the tatweel's.
p.adv -= l;
}
out.push(p);
if let Some(l) = leader {
tab_leader(g, l, tab_x, tab_w, tab_origin, &mut out);
@ -1680,6 +1686,7 @@ fn reorder_visual(line: &[Glyph], out: &mut [PlacedGlyph], pens: &[f64], start:
let u = &units[k];
for i in u.clone() {
shifts[i] = cur - pens[u.start];
out[i].rtl = levels[k].is_rtl();
}
cur += width(u);
}
@ -1720,6 +1727,7 @@ fn tab_leader(tab: &Glyph, leader: &str, x: f64, w: f64, origin: f64, out: &mut
visible: true,
upright: false,
tcy: None,
rtl: false,
});
at += adv;
}
@ -1822,6 +1830,7 @@ fn place(g: &Glyph, x: f64) -> PlacedGlyph {
visible,
upright: upright_in_vertical(g.ch),
tcy: g.tcy,
rtl: false,
}
}
@ -1991,6 +2000,12 @@ fn vertical_justify(ft: &mut FrameText, f: &FrameSpec) {
/// Caret geometry for story byte `pos`: (frame index, x, baseline, ascent, descent).
pub fn caret(cs: &ComposedStory, pos: usize) -> Option<(usize, f64, f64, f64, f64)> {
let (fi, l) = caret_line(cs, pos)?;
Some((fi, caret_x(l, pos), l.baseline, l.ascent, l.descent))
}
/// The line the caret at `pos` is drawn on (and its frame index).
fn caret_line(cs: &ComposedStory, pos: usize) -> Option<(usize, &Line)> {
let mut best: Option<(usize, &Line)> = None;
for (fi, ft) in cs.frames.iter().enumerate() {
for l in &ft.lines {
@ -2009,22 +2024,54 @@ pub fn caret(cs: &ComposedStory, pos: usize) -> Option<(usize, f64, f64, f64, f6
break;
}
}
let (fi, l) = best?;
Some((fi, caret_x(l, pos), l.baseline, l.ascent, l.descent))
best
}
fn caret_x(l: &Line, pos: usize) -> f64 {
/// The caret's x on line `l` for story byte `pos` (bidi-aware).
pub fn caret_x(l: &Line, pos: usize) -> f64 {
for g in &l.glyphs {
if g.len > 0 && pos >= g.byte && pos < g.byte + g.len {
// Inside a multi-char cluster (ligature): interpolate.
let t = (pos - g.byte) as f64 / g.len as f64;
return g.x + g.adv * t;
return if g.rtl { g.x + g.adv * (1.0 - t) } else { g.x + g.adv * t };
}
if g.len > 0 && g.byte >= pos {
return g.x;
return if g.rtl { g.x + g.adv } else { g.x };
}
}
l.end_x
match l.glyphs.iter().rev().find(|g| g.len > 0) {
Some(g) if g.rtl => g.x,
_ => l.end_x,
}
}
/// Caret positions on a line with their x, for lines with right-to-left text.
fn caret_stops(l: &Line) -> Vec<(usize, f64)> {
let mut ps: Vec<usize> = l.glyphs.iter().filter(|g| g.len > 0).flat_map(|g| [g.byte, g.byte + g.len]).collect();
ps.extend([l.range.start, l.range.end]);
ps.sort_unstable();
ps.dedup();
ps.into_iter().filter(|p| l.range.contains(p) || *p == l.range.end).map(|p| (p, caret_x(l, p))).collect()
}
/// Bidi caret movement: the position one step to the left (or right) of `pos` on its line as
/// drawn. `None` when the line has no right-to-left text or `pos` is at that side's edge of the
/// line: then move in text order ([`line_rtl`] says which way that runs).
pub fn visual_step(cs: &ComposedStory, pos: usize, left: bool) -> Option<usize> {
let (_, l) = caret_line(cs, pos)?;
if !l.glyphs.iter().any(|g| g.rtl) {
return None;
}
let x = caret_x(l, pos);
let stops = caret_stops(l);
let side = |s: &&(usize, f64)| if left { s.1 < x - 0.01 } else { s.1 > x + 0.01 };
let pick = stops.iter().filter(side);
if left { pick.max_by(|a, b| a.1.total_cmp(&b.1)) } else { pick.min_by(|a, b| a.1.total_cmp(&b.1)) }.map(|s| s.0)
}
/// Does the line at `pos` start (in text order) with right-to-left text?
pub fn line_rtl(cs: &ComposedStory, pos: usize) -> bool {
caret_line(cs, pos).and_then(|(_, l)| l.glyphs.iter().find(|g| g.len > 0)).is_some_and(|g| g.rtl)
}
/// Story byte nearest to point `p` (frame inner space) in frame `fi`.
@ -2040,6 +2087,10 @@ pub fn hit(cs: &ComposedStory, fi: usize, p: Point) -> Option<usize> {
.filter(|l| p.x >= l.x0 - 20.0 && p.x <= l.x1 + 20.0 || ft.columns.len() <= 1)
.min_by(|a, b| line_dist(a, p.y).total_cmp(&line_dist(b, p.y)))
.or_else(|| ft.lines.first())?;
if l.glyphs.iter().any(|g| g.rtl) {
// Bidi: the caret position drawn nearest the point.
return caret_stops(l).into_iter().min_by(|a, b| (a.1 - p.x).abs().total_cmp(&(b.1 - p.x).abs())).map(|s| s.0);
}
let mut best = l.range.start;
let mut prev_mid = f64::NEG_INFINITY;
for g in l.glyphs.iter().filter(|g| g.len > 0) {

View File

@ -841,6 +841,34 @@ fn bidi_matches_the_reference_order() {
}
}
#[test]
fn carets_in_right_to_left_text_follow_the_drawing() {
let mut d = Document::new(&designcraft_doc::build::NewDocument::default());
let lid = d.default_layer();
// "abc" then an Arabic word, in a left-to-right paragraph.
let text = "abc سلام";
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(100.0, 100.0, 400.0, 200.0), lid, text, ParaFormat::default()).unwrap();
let cs = compose_story(&d, sid, &ComposeOptions::default());
let l = &cs.frames[0].lines[0];
let ar = text.find('س').unwrap();
assert!(l.glyphs.iter().filter(|g| g.len > 0 && g.byte >= ar).all(|g| g.rtl));
assert!(l.glyphs.iter().filter(|g| g.byte < 3).all(|g| !g.rtl));
let x = |p: usize| caret(&cs, p).unwrap().1;
// The Arabic word starts at its right edge and runs left.
let after = ar + 'س'.len_utf8();
assert!(x(after) < x(ar), "{} {}", x(after), x(ar));
assert!(x(text.len()) < x(ar));
// Arrow keys move as drawn: the word's start is at its right edge, so Left steps into it.
assert_eq!(visual_step(&cs, ar, true), Some(after));
assert_eq!(visual_step(&cs, after, false), Some(ar));
// Latin text has no right-to-left glyphs.
let (_, s2) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(100.0, 300.0, 400.0, 400.0), lid, "abc", ParaFormat::default()).unwrap();
assert_eq!(visual_step(&compose_story(&d, s2, &ComposeOptions::default()), 1, true), None);
// A click at the word's right edge puts the caret at its start.
let hx = x(ar) - 0.1;
assert_eq!(hit(&cs, 0, Point::new(hx, l.baseline)), Some(ar));
}
#[test]
fn kashidas_stretch_justified_arabic_before_spaces() {
let mut d = Document::new(&designcraft_doc::build::NewDocument::default());

View File

@ -287,6 +287,7 @@ impl Document {
}
}
let facing = self.settings.facing_pages;
let rtl = facing && self.settings.right_to_left_binding;
let mut spreads: Vec<Spread> = Vec::new();
let mut cur: Vec<(Page, Vec<Arc<Item>>)> = Vec::new();
let total = pages.len();
@ -295,27 +296,29 @@ impl Document {
return;
}
let mut sp = Spread { id, pages: vec![], items: vec![], parent: None, allow_shuffle: true };
let mut x = 0.0;
let n = cur.len();
let mut all_items = Vec::new();
for (i, (mut p, items)) in cur.drain(..).enumerate() {
p.side = if !facing {
PageSide::Single
} else if n == 1 {
// Lone page: right if odd page number position, decided by caller via side preset.
p.side
} else if i == 0 {
} else if (i == 0) != rtl {
PageSide::Left
} else {
PageSide::Right
};
p.x = x;
all_items.push(items);
sp.pages.push(p);
}
sp.relayout();
for (p, items) in sp.pages.iter().zip(all_items) {
for it in items {
let mut it = Arc::unwrap_or_clone(it);
it.xf = Affine::translate((x, 0.0)) * it.xf;
it.xf = Affine::translate((p.x, 0.0)) * it.xf;
sp.items.push(Arc::new(it));
}
x += p.width;
sp.pages.push(p);
}
spreads.push(sp);
};
@ -324,8 +327,11 @@ impl Document {
for (i, (mut p, items)) in pages.into_iter().enumerate() {
if facing {
// Page i (0-based) is a right page when its number is odd (1, 3, 5 …).
p.side = if (i + shift) % 2 == 0 { PageSide::Right } else { PageSide::Left };
if p.side == PageSide::Left && !cur.is_empty() {
// Bound right to left, odd pages are left pages and spreads start on the right.
let odd = (i + shift) % 2 == 0;
p.side = if odd != rtl { PageSide::Right } else { PageSide::Left };
let starts = if rtl { PageSide::Right } else { PageSide::Left };
if p.side == starts && !cur.is_empty() {
let id = SpreadId(self.alloc());
flush(&mut cur, &mut spreads, id);
}

View File

@ -296,6 +296,9 @@ pub struct DocSettings {
pub page_width: f64,
pub page_height: f64,
pub facing_pages: bool,
/// Binding: Right to Left — page 1 is a left page and spreads read from right to left.
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub right_to_left_binding: bool,
pub primary_text_frame: bool,
/// Bleed: top, bottom, inside, outside.
pub bleed: [f64; 4],
@ -339,6 +342,7 @@ impl Default for DocSettings {
page_width: 612.0,
page_height: 792.0,
facing_pages: true,
right_to_left_binding: false,
primary_text_frame: false,
bleed: [0.0; 4],
slug: [0.0; 4],

View File

@ -291,6 +291,8 @@ impl Spread {
pub fn spine_x(&self) -> f64 {
match self.pages.iter().position(|p| p.side == PageSide::Right) {
Some(i) => self.pages[i].x,
// Left pages only (a right-to-left document's first page): the spine is on their right.
None if !self.pages.is_empty() && self.pages.iter().all(|p| p.side == PageSide::Left) => self.bounds().x1,
None => self.bounds().center().x,
}
}
@ -299,14 +301,26 @@ impl Spread {
if self.pages.is_empty() {
return None;
}
self.pages
.iter()
.position(|p| x >= p.x && x < p.x + p.width)
.or_else(|| if x < self.pages[0].x { Some(0) } else { Some(self.pages.len() - 1) })
self.pages.iter().position(|p| x >= p.x && x < p.x + p.width).or_else(|| {
// The pasteboard: the page nearest that side.
let (l, r) = (0..self.pages.len()).fold((0, 0), |(l, r), i| {
(if self.pages[i].x < self.pages[l].x { i } else { l }, if self.pages[i].x > self.pages[r].x { i } else { r })
});
Some(if x < self.pages[l].x { l } else { r })
})
}
/// Lay the pages out left to right with no gaps, starting at x = 0.
/// Lay the pages out with no gaps from x = 0: left to right, or right to left for a
/// right-to-left bound pair (its first page is the right one).
pub fn relayout(&mut self) {
let rtl = self.pages.len() == 2 && self.pages[0].side == PageSide::Right && self.pages[1].side == PageSide::Left;
let mut x = 0.0;
if rtl {
for p in self.pages.iter_mut().rev() {
p.x = x;
x += p.width;
}
return;
}
for p in &mut self.pages {
p.x = x;
x += p.width;

View File

@ -232,7 +232,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Document Setup…",
["File"],
Some("Cmd+Alt+P"),
"{width?, height?, pages?: count, startPage?: n, facingPages?, intent?: print|web|mobile, bleed?, slug?: n | [top, bottom, inside, outside], adjustLayout?: bool (objects follow the new page size)} → the document setup",
"{width?, height?, pages?: count, startPage?: n, facingPages?, binding?: leftToRight|rightToLeft, intent?: print|web|mobile, bleed?, slug?: n | [top, bottom, inside, outside], adjustLayout?: bool (objects follow the new page size)} → the document setup",
has_doc,
|s, p| {
let p = p.clone();
@ -257,6 +257,9 @@ pub fn specs() -> Vec<CommandSpec> {
if let Some(f) = p.get("facingPages").and_then(Value::as_bool) {
d.settings.facing_pages = f;
}
if let Some(b) = p.get("binding").and_then(Value::as_str) {
d.settings.right_to_left_binding = b == "rightToLeft";
}
if let Some(b) = bleed {
d.settings.bleed = b;
}
@ -702,7 +705,7 @@ fn document_setup(d: &designcraft_doc::Document) -> Value {
let st = &d.settings;
let start = d.sections.iter().find(|x| x.start == 0).and_then(|x| x.start_number).unwrap_or(1);
json!({"width": st.page_width, "height": st.page_height, "pages": d.page_count(), "startPage": start, "facingPages": st.facing_pages,
"intent": st.intent, "bleed": st.bleed, "slug": st.slug})
"binding": if st.right_to_left_binding { "rightToLeft" } else { "leftToRight" }, "intent": st.intent, "bleed": st.bleed, "slug": st.slug})
}
#[cfg(test)]
@ -730,6 +733,32 @@ mod setup_tests {
assert_eq!(r["pages"], 3);
s.doc().unwrap().doc.check().unwrap();
}
#[test]
fn right_to_left_binding_reverses_spreads() {
use designcraft_doc::PageSide::{Left, Right};
let mut s = crate::Session::new();
s.execute("file.new", &json!({"pages": 4})).unwrap();
// A frame on page 2 stays on page 2.
let w = s.doc().unwrap().doc.settings.page_width;
let f = s.execute("frame.create", &json!({"rect": [w + 72.0, 72, w + 200.0, 200], "content": "text", "text": "two"})).unwrap();
let r = s.execute("layout.documentSetup", &json!({"binding": "rightToLeft"})).unwrap();
assert_eq!(r["binding"], "rightToLeft");
let d = &s.doc().unwrap().doc;
let shape: Vec<Vec<(designcraft_doc::PageSide, f64)>> = d.spreads.iter().map(|sp| sp.pages.iter().map(|p| (p.side, p.x)).collect()).collect();
// Page 1 alone on the left; pages 2–3 with 2 on the right; page 4 alone on the right.
assert_eq!(shape, vec![vec![(Left, 0.0)], vec![(Right, w), (Left, 0.0)], vec![(Right, 0.0)]]);
let (si, _) = d.page_loc(1).unwrap();
assert_eq!(si, 1);
let it = d.item(designcraft_doc::ItemId(f["id"].as_u64().unwrap())).unwrap();
assert_eq!(d.spreads[si].page_at_x(it.bounds().center().x), Some(0), "the frame is still on page 2");
assert!(it.bounds().x0 >= w, "page 2 is the right page now");
d.check().unwrap();
// And back.
s.execute("layout.documentSetup", &json!({"binding": "leftToRight"})).unwrap();
let d = &s.doc().unwrap().doc;
assert_eq!(d.spreads[1].pages.iter().map(|p| (p.side, p.x)).collect::<Vec<_>>(), vec![(Left, 0.0), (Right, w)]);
}
}
#[cfg(test)]

View File

@ -565,18 +565,22 @@ fn move_caret(s: &mut Session, p: &Value) -> Result<Value> {
let new = match dir.as_str() {
"left" if !extend && !t.is_caret() => collapse_to(true),
"right" if !extend && !t.is_caret() => collapse_to(false),
"left" => {
if word {
prev_word(&text, pos)
} else {
prev_char(&text, pos)
}
}
"right" => {
if word {
next_word(&text, pos)
} else {
next_char(&text, pos)
"left" | "right" => {
// Lines with right-to-left text move as drawn; at a line's edge, or by words, a
// right-to-left line's "left" is forward in the text.
let left = dir == "left";
let visual = if word { None } else { compose::visual_step(&cs, pos, left) };
match visual {
Some(p) => p,
None => {
let forward = left == compose::line_rtl(&cs, pos);
match (forward, word) {
(true, true) => next_word(&text, pos),
(true, false) => next_char(&text, pos),
(false, true) => prev_word(&text, pos),
(false, false) => prev_char(&text, pos),
}
}
}
}
"up" | "down" => vertical(&cs, pos, dir == "up").unwrap_or(pos),
@ -1206,3 +1210,28 @@ mod tcy_tests {
assert_eq!(get(&s), Some(true));
}
}
#[cfg(test)]
mod bidi_caret_tests {
use serde_json::json;
use crate::Session;
#[test]
fn arrow_keys_in_a_right_to_left_paragraph() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "سلام عليكم"})).unwrap();
let sid = r["story"].as_u64().unwrap();
s.execute("type.para", &json!({"attrs": {"direction": "rightToLeft"}})).unwrap();
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap();
// Left moves forward through right-to-left text, Right back.
let p = s.execute("text.move", &json!({"dir": "left"})).unwrap()["pos"].as_u64().unwrap();
assert_eq!(p, 'س'.len_utf8() as u64);
let p = s.execute("text.move", &json!({"dir": "right"})).unwrap()["pos"].as_u64().unwrap();
assert_eq!(p, 0);
// By words too.
let p = s.execute("text.move", &json!({"dir": "left", "word": true})).unwrap()["pos"].as_u64().unwrap();
assert!(p >= "سلام".len() as u64, "{p}");
}
}

View File

@ -678,7 +678,7 @@ impl<'a> Ex<'a> {
.attr("PreserveLayoutWhenShuffling", "true")
.attr("AllowPageShuffle", "true")
.attr("OverprintBlack", bool_s(s.overprint_black))
.attr("PageBinding", "LeftToRight")
.attr("PageBinding", if s.right_to_left_binding { "RightToLeft" } else { "LeftToRight" })
.attr("ColumnDirection", "Horizontal")
.attr(
"Intent",

View File

@ -414,6 +414,9 @@ impl<'r> Importer<'r> {
if let Some(v) = e.boolean("FacingPages") {
s.facing_pages = v;
}
if let Some(v) = e.get("PageBinding") {
s.right_to_left_binding = v == "RightToLeft";
}
for (i, k) in ["DocumentBleedTopOffset", "DocumentBleedBottomOffset", "DocumentBleedInsideOrLeftOffset", "DocumentBleedOutsideOrRightOffset"]
.iter()
.enumerate()

View File

@ -976,14 +976,32 @@ fn draw_text_selection(
if s > e || (s == e && !(l.range.end < range.end && e == l.range.end)) {
continue;
}
let x0 = caret_x(l, s);
let x1 = if e == l.range.end && range.end > l.range.end { l.end_x.max(x0 + 3.0) } else { caret_x(l, e) };
quads.push([
Point::new(x0, l.baseline - l.ascent),
Point::new(x1, l.baseline - l.ascent),
Point::new(x1, l.baseline + l.descent),
Point::new(x0, l.baseline + l.descent),
]);
let quad = |x0: f64, x1: f64| {
[
Point::new(x0, l.baseline - l.ascent),
Point::new(x1, l.baseline - l.ascent),
Point::new(x1, l.baseline + l.descent),
Point::new(x0, l.baseline + l.descent),
]
};
if l.glyphs.iter().any(|g| g.rtl) {
// Mixed directions: the selected glyphs' boxes, joined where they touch.
let mut spans: Vec<(f64, f64)> =
l.glyphs.iter().filter(|g| g.len > 0 && g.byte >= s && g.byte < e).map(|g| (g.x, g.x + g.adv)).collect();
spans.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut merged: Vec<(f64, f64)> = Vec::new();
for (a, b) in spans {
match merged.last_mut() {
Some(m) if a <= m.1 + 0.5 => m.1 = m.1.max(b),
_ => merged.push((a, b)),
}
}
quads.extend(merged.into_iter().map(|(a, b)| quad(a, b)));
} else {
let x0 = compose::caret_x(l, s);
let x1 = if e == l.range.end && range.end > l.range.end { l.end_x.max(x0 + 3.0) } else { compose::caret_x(l, e) };
quads.push(quad(x0, x1));
}
for (gi, g) in l.glyphs.iter().enumerate() {
if g.visible && g.len > 0 && g.byte >= s && g.byte < e {
glyphs.push((li, gi));
@ -1044,8 +1062,8 @@ fn draw_cell_selection(
if s > e || (s == e && !(l.range.end < range.end && e == l.range.end)) {
continue;
}
let x0 = caret_x(l, s);
let x1 = if e == l.range.end && range.end > l.range.end { l.end_x.max(x0 + 3.0) } else { caret_x(l, e) };
let x0 = compose::caret_x(l, s);
let x1 = if e == l.range.end && range.end > l.range.end { l.end_x.max(x0 + 3.0) } else { compose::caret_x(l, e) };
let q = [
xf.to_screen(m * Point::new(x0, l.baseline - l.ascent)),
xf.to_screen(m * Point::new(x1, l.baseline - l.ascent)),
@ -1162,18 +1180,6 @@ fn draw_inverse_highlight(
painter.image(tex.id(), bbox, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
fn caret_x(l: &compose::Line, pos: usize) -> f64 {
for g in &l.glyphs {
if g.len > 0 && pos >= g.byte && pos < g.byte + g.len {
return g.x + g.adv * (pos - g.byte) as f64 / g.len as f64;
}
if g.len > 0 && g.byte >= pos {
return g.x;
}
}
l.end_x
}
fn draw_tool_overlays(app: &mut DesignApp, painter: &egui::Painter, xf: &Xf) {
if let (Some((z, c)), Some(rect)) = (app.power_zoom, app.canvas_rect) {
// The view that releasing returns to.

View File

@ -274,7 +274,7 @@ pub fn open_document_setup(app: &mut DesignApp) {
let units = app.session.active().map(|s| s.doc.settings.horizontal_units).unwrap_or(Unit::Picas);
let fm = |v: &Value| json!(format_measure(v.as_f64().unwrap_or(0.0), units));
let mut f = json!({"intent": cur["intent"], "pages": cur["pages"], "startPage": cur["startPage"], "facingPages": cur["facingPages"],
"width": fm(&cur["width"]), "height": fm(&cur["height"])});
"binding": cur["binding"], "width": fm(&cur["width"]), "height": fm(&cur["height"])});
for k in ["bleed", "slug"] {
for (i, e) in ["Top", "Bottom", "Inside", "Outside"].iter().enumerate() {
f[format!("{k}{e}")] = fm(&cur[k][i]);
@ -312,6 +312,19 @@ fn document_setup(ui: &mut egui::Ui, d: &mut Dialog) {
ui.label("Start Page #:");
text_field(ui, d, "startPage", 60.0);
ui.end_row();
ui.label("Binding:");
let rtl = d.s("binding") == "rightToLeft";
egui::ComboBox::from_id_salt("ds_binding").selected_text(if rtl { "Right to Left" } else { "Left to Right" }).width(110.0).show_ui(
ui,
|ui| {
for (v, l) in [("leftToRight", "Left to Right"), ("rightToLeft", "Right to Left")] {
if ui.selectable_label((v == "rightToLeft") == rtl, l).clicked() {
d.fields.insert("binding".into(), json!(v));
}
}
},
);
ui.end_row();
});
ui.add_space(8.0);
ui.label(egui::RichText::new("Page Size").font(semibold(12.0)));
@ -1309,7 +1322,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
app.run(
"layout.documentSetup",
json!({"intent": d.s("intent"), "pages": d.n("pages").unwrap_or(1.0).max(1.0) as u64, "startPage": d.n("startPage").unwrap_or(1.0).max(1.0) as u64,
"facingPages": d.b("facingPages"), "width": d.m("width"), "height": d.m("height"), "bleed": edges("bleed"), "slug": edges("slug"), "adjustLayout": d.b("adjustLayout")}),
"facingPages": d.b("facingPages"), "binding": d.s("binding"), "width": d.m("width"), "height": d.m("height"), "bleed": edges("bleed"), "slug": edges("slug"), "adjustLayout": d.b("adjustLayout")}),
)
}
"paragraphStyleOptions" => {

View File

@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|---|---|---|---|---|---|
| P0 | 102 | 100 | 2 | 0 | 99% |
| P1 | 97 | 96 | 1 | 0 | 99% |
| P2 | 76 | 71 | 5 | 0 | 97% |
| P2 | 76 | 72 | 4 | 0 | 97% |
| Area | Parity |
|---|---|
@ -214,7 +214,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
| Type & text frames | Drag & drop text editing | P1 | D |
| Type & text frames | Paste text with/without formatting | P0 | D |
| Type & text frames | Math expressions | P2 | D |
| Type & text frames | Right-to-left / World-Ready composer | P2 | P |
| Type & text frames | Right-to-left / World-Ready composer | P2 | D |
| Typography | Character attributes | P0 | D |
| Typography | Underline/Strikethrough options | P1 | D |
| Typography | OpenType features | P0 | D |