mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Type: handle soft return in no-break runs, event modifiers, trailing breaks, and tabs
This commit is contained in:
committed by
bflatastic
parent
d89031848c
commit
b6df1ef0b1
@@ -55,7 +55,7 @@ pub(crate) fn is_word_space(g: &SGlyph) -> bool {
|
||||
|
||||
/// A glyph letter spacing follows: a visible glyph followed by another one in the same word.
|
||||
fn letter_gap(g: &SGlyph, next: Option<&SGlyph>) -> bool {
|
||||
!g.is_space() && g.inline.is_none() && next.is_some_and(|n| !n.is_space())
|
||||
!g.is_space() && g.ch != '\u{2028}' && g.inline.is_none() && next.is_some_and(|n| !n.is_space() && n.ch != '\u{2028}')
|
||||
}
|
||||
|
||||
/// The width of a word space in `g`'s font and size: what letter spacing is a share of.
|
||||
@@ -83,7 +83,7 @@ pub(crate) fn apply_desired(sg: &mut [SGlyph], j: &JustificationOptions, scale_g
|
||||
f.word = (base * (w_max - w_des).max(0.0), base * (w_des - w_min).max(0.0));
|
||||
continue;
|
||||
}
|
||||
if g.is_space() || g.inline.is_some() {
|
||||
if g.is_space() || g.inline.is_some() || g.ch == '\u{2028}' {
|
||||
continue;
|
||||
}
|
||||
let base = g.adv;
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Pen<'_> {
|
||||
pub(super) fn tab_advance<'t>(tabs: &'t [vectorcraft_doc::TabStop], origin: f64, x: f64, rest: &[SGlyph]) -> (f64, &'t str) {
|
||||
use vectorcraft_doc::TabAlign;
|
||||
let rel = x - origin;
|
||||
let seg: Vec<&SGlyph> = rest.iter().take_while(|g| g.ch != '\t').collect();
|
||||
let seg: Vec<&SGlyph> = rest.iter().take_while(|g| g.ch != '\t' && g.ch != '\u{2028}').collect();
|
||||
let width: f64 = seg.iter().map(|g| g.adv).sum();
|
||||
let target = |pos: f64, align: TabAlign, on: char| match align {
|
||||
TabAlign::Left => pos,
|
||||
@@ -300,9 +300,9 @@ pub(super) fn break_line(
|
||||
cluster_end += 1;
|
||||
}
|
||||
// No Break: the marked characters stay on one line, including across a space inside the run.
|
||||
if gl.no_break {
|
||||
if gl.no_break && gl.ch != '\u{2028}' {
|
||||
while let Some(n) = g.get(cluster_end) {
|
||||
if !n.no_break {
|
||||
if !n.no_break || n.ch == '\u{2028}' {
|
||||
break;
|
||||
}
|
||||
let b = n.byte;
|
||||
|
||||
@@ -390,6 +390,7 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
|
||||
let mut hyphen_run = 0;
|
||||
let mut line_in_paragraph = 0;
|
||||
let mut index = 0;
|
||||
let mut emitted_empty_continuation = false;
|
||||
loop {
|
||||
let mut placement = PlacementContext {
|
||||
para: paragraph.para,
|
||||
@@ -415,7 +416,13 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
|
||||
line_in_paragraph += 1;
|
||||
index = geometry.position.end;
|
||||
if index >= n {
|
||||
break;
|
||||
if emitted_empty_continuation {
|
||||
break;
|
||||
}
|
||||
emitted_empty_continuation = paragraph.sg.last().is_some_and(|g| g.ch == '\u{2028}');
|
||||
if !emitted_empty_continuation {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pen.pending += paragraph.para.space_after;
|
||||
@@ -794,7 +801,7 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
} else {
|
||||
position.m.first_baseline(pen.fb, pen.fb_min)
|
||||
},
|
||||
start: if index < sg.len() { sg[index].byte } else { paragraph.pr.start },
|
||||
start: if index < sg.len() { sg[index].byte } else { paragraph.pr.end },
|
||||
end: if position.end >= sg.len() { paragraph.pr.end } else { sg[position.end].byte },
|
||||
glyph_start,
|
||||
glyph_end: cx.out.glyphs.len(),
|
||||
@@ -802,8 +809,13 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
|
||||
region: if paragraph.regions.is_some() { pen.ri } else { 0 },
|
||||
});
|
||||
pen.settled(position.baseline, position.m);
|
||||
// Further spans of this line band share the settled baseline.
|
||||
for queued in &mut pen.queued {
|
||||
queued.0 = position.baseline;
|
||||
let forced_break = position.end > index && sg.get(position.end - 1).is_some_and(|g| g.ch == '\u{2028}');
|
||||
if forced_break {
|
||||
pen.queued.clear();
|
||||
} else {
|
||||
// Further spans of this line band share the settled baseline.
|
||||
for queued in &mut pen.queued {
|
||||
queued.0 = position.baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,10 @@ impl SGlyph {
|
||||
}
|
||||
/// A line may break after this glyph.
|
||||
pub fn break_after(&self) -> bool {
|
||||
self.is_space() || self.word_end || matches!(self.ch, '-' | '\u{2010}' | '\u{2013}' | '\u{2014}' | '/' | SOFT_HYPHEN) || is_cjk(self.ch)
|
||||
self.is_space()
|
||||
|| self.word_end
|
||||
|| matches!(self.ch, '-' | '\u{2010}' | '\u{2013}' | '\u{2014}' | '/' | SOFT_HYPHEN | '\u{2028}')
|
||||
|| is_cjk(self.ch)
|
||||
}
|
||||
/// A soft (discretionary) hyphen: invisible unless a line breaks after it.
|
||||
pub fn is_soft_hyphen(&self) -> bool {
|
||||
|
||||
@@ -1377,3 +1377,20 @@ fn soft_return_in_list_does_not_repeat_marker() {
|
||||
assert!(l0_glyphs.iter().any(|g| g.len == 0), "line 0 has bullet marker");
|
||||
assert!(!l1_glyphs.iter().any(|g| g.len == 0), "continuation line does not have bullet marker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_soft_return_creates_empty_continuation_line() {
|
||||
let t = TextObject::point(Point::ZERO, "Line 1\u{2028}", style(12.0));
|
||||
let lay = layout(db(), &t);
|
||||
assert_eq!(lay.lines.len(), 2, "trailing soft return produces a second empty line");
|
||||
assert_eq!(lay.line_of(t.plain_text().len()), 1, "caret at end sits on the new line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soft_return_breaks_line_even_inside_no_break() {
|
||||
let mut st = style(12.0);
|
||||
st.no_break = true;
|
||||
let t = area("First\u{2028}Second", st, Rect::new(0.0, 0.0, 300.0, 200.0), Justify::Left);
|
||||
let lay = layout(db(), &t);
|
||||
assert_eq!(lay.lines.len(), 2, "soft return honors line break despite no_break");
|
||||
}
|
||||
|
||||
@@ -210,7 +210,18 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
// A key the tool claims (Esc with a loaded place cursor) is only the tool's.
|
||||
let claimed = app.session.tool_claims_key(tk, view);
|
||||
let mods = ctx.input(|i| crate::canvas::mods(i.modifiers, false));
|
||||
let mods = ctx.input(|i| {
|
||||
let m = i
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|e| match e {
|
||||
egui::Event::Key { key, pressed: true, modifiers, .. } if *key == k => Some(*modifiers),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(i.modifiers);
|
||||
crate::canvas::mods(m, false)
|
||||
});
|
||||
let r = app.session.tool_key(tk, mods, view);
|
||||
// Enter also takes what the tool asks of the UI: Rotate, Scale, Reflect and Shear open
|
||||
// their dialog with it.
|
||||
|
||||
Reference in New Issue
Block a user