mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #3136 from nrq6993-a11y/stream/vc-feat1-g4-mixed-paragraph-leading
G4: Paragraph panel indents and spacing show blank when paragraphs differ
This commit is contained in:
@@ -68,7 +68,7 @@ fn shared_effective(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> f64) -> Opti
|
||||
shared_styles(app, |t, style| t.effective_char_style(style).map(|s| Metric(f(&s)))).map(|v| v.0)
|
||||
}
|
||||
|
||||
struct Metric(f64);
|
||||
pub(crate) struct Metric(pub(crate) f64);
|
||||
|
||||
impl PartialEq for Metric {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
@@ -109,21 +109,21 @@ fn shared_styles<T: PartialEq>(app: &VectorcraftApp, f: impl Fn(&vectorcraft_doc
|
||||
}
|
||||
|
||||
/// The value a run of styles share so far ([`shared`]).
|
||||
enum Shared<T> {
|
||||
pub(crate) enum Shared<T> {
|
||||
None,
|
||||
Same(T),
|
||||
Differ,
|
||||
}
|
||||
|
||||
impl<T: PartialEq> Shared<T> {
|
||||
fn add(&mut self, v: T) {
|
||||
pub(crate) fn add(&mut self, v: T) {
|
||||
match self {
|
||||
Shared::None => *self = Shared::Same(v),
|
||||
Shared::Same(x) if *x != v => *self = Shared::Differ,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn value(self) -> Option<T> {
|
||||
pub(crate) fn value(self) -> Option<T> {
|
||||
match self {
|
||||
Shared::Same(v) => Some(v),
|
||||
_ => None,
|
||||
|
||||
@@ -7,7 +7,7 @@ use egui::{Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Composer, HyphenationOptions, Justify, NodeKind, ParaDirection, ParaStyle};
|
||||
|
||||
use super::character::text_style;
|
||||
use super::character::{Metric, Shared, text_style};
|
||||
use super::{pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
@@ -142,6 +142,43 @@ fn collect_text_alignment(text: &vectorcraft_doc::TextObject, range: Option<(usi
|
||||
record(index, &plain, start, end) // Includes empty text and the paragraph after a final newline.
|
||||
}
|
||||
|
||||
/// Visit every paragraph the Paragraph panel acts on.
|
||||
fn visit_paragraphs(app: &VectorcraftApp, mut f: impl FnMut(&ParaStyle)) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
if let Some((id, a, b)) = super::character::text_editing(app)
|
||||
&& let Some(NodeKind::Text(text)) = st.doc.node(id).map(|n| &n.kind)
|
||||
{
|
||||
let range = text.paragraphs_in(a, b);
|
||||
for i in range {
|
||||
f(text.para_at(i));
|
||||
}
|
||||
return;
|
||||
}
|
||||
for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
|
||||
node.walk(&mut |c| {
|
||||
if let NodeKind::Text(t) = &c.kind {
|
||||
for i in 0..t.paragraph_count() {
|
||||
f(t.para_at(i));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A distance shared by every paragraph in scope, or `None` when they differ (blank spin field).
|
||||
fn shared_para_field(app: &VectorcraftApp, field: impl Fn(&ParaStyle) -> f64) -> Option<f64> {
|
||||
let mut acc = Shared::None;
|
||||
let mut any = false;
|
||||
visit_paragraphs(app, |p| {
|
||||
any = true;
|
||||
acc.add(Metric(field(p)));
|
||||
});
|
||||
if !any {
|
||||
return text_style(app).map(|(_, p)| field(&p));
|
||||
}
|
||||
acc.value().map(|m| m.0)
|
||||
}
|
||||
|
||||
/// None means differing displayed alignments, following the other mixed-value controls.
|
||||
fn selected_alignment(app: &VectorcraftApp, fallback: &ParaStyle) -> Option<Justify> {
|
||||
let default = Some(shown_alignment(fallback.justify, fallback.direction == Some(ParaDirection::RightToLeft)));
|
||||
@@ -217,27 +254,27 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
};
|
||||
egui::Grid::new("para-grid").num_columns(4).spacing([4.0, 4.0]).show(ui, |ui| {
|
||||
label(ui, "→|", tl!("Left Indent"));
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-li", Some(para.left_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-li", shared_para_field(app, |p| p.left_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"leftIndent": v}));
|
||||
}
|
||||
label(ui, "|←", tl!("Right Indent"));
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-ri", Some(para.right_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-ri", shared_para_field(app, |p| p.right_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"rightIndent": v}));
|
||||
}
|
||||
ui.end_row();
|
||||
label(ui, "1→", tl!("First-line Left Indent"));
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-fi", Some(para.first_line_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-fi", shared_para_field(app, |p| p.first_line_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"firstLineIndent": v}));
|
||||
}
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
label(ui, "↑¶", tl!("Space Before Paragraph"));
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sb", Some(para.space_before), unit, fw, 1.0, 0.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sb", shared_para_field(app, |p| p.space_before), unit, fw, 1.0, 0.0, &[]) {
|
||||
format(app, json!({"spaceBefore": v}));
|
||||
}
|
||||
label(ui, "↓¶", tl!("Space After Paragraph"));
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sa", Some(para.space_after), unit, fw, 1.0, 0.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sa", shared_para_field(app, |p| p.space_after), unit, fw, 1.0, 0.0, &[]) {
|
||||
format(app, json!({"spaceAfter": v}));
|
||||
}
|
||||
ui.end_row();
|
||||
@@ -442,6 +479,23 @@ mod tests {
|
||||
app.session.set_tool_option("select", &json!({"start": start, "end": end}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_mixed_indent_fields_show_blank() {
|
||||
let (mut app, id) = editing_text("A\nB\n");
|
||||
app.run("text.setFormat", json!({"id": id, "leftIndent": 10})).unwrap();
|
||||
app.run("text.setFormat", json!({"id": id, "start": 2, "end": 3, "leftIndent": 20})).unwrap();
|
||||
app.session.set_tool_option("selectAll", &json!(true));
|
||||
assert_eq!(shared_para_field(&app, |p| p.left_indent), None);
|
||||
assert_eq!(shared_para_field(&app, |p| p.space_before), Some(0.0));
|
||||
app.run("text.setFormat", json!({"id": id, "leftIndent": 12})).unwrap();
|
||||
assert_eq!(shared_para_field(&app, |p| p.left_indent), Some(12.0));
|
||||
app.session.set_tool_option("stopEditing", &json!(true));
|
||||
let other = app.run("text.create", json!({"x": 40, "y": 80, "text": "C"})).unwrap()["id"].clone();
|
||||
app.run("text.setFormat", json!({"id": other, "leftIndent": 5})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id, other]})).unwrap();
|
||||
assert_eq!(shared_para_field(&app, |p| p.left_indent), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_mixed_alignment_click_center_and_undo() {
|
||||
let (mut app, id) = editing_text("é\nאב\n終");
|
||||
|
||||
@@ -201,6 +201,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
- **Done:** character-style Auto-leading resets propagate to style users while retaining local leading overrides. Sparse definitions remain sparse, native serialization retains explicit Auto, and the Character Styles override marker stays consistent.
|
||||
|
||||
- **Improved:** mixed paragraph alignments leave the existing Paragraph/Properties alignment buttons unselected; choosing an alignment still uses the range-aware command. Headless regressions, full integration CI and visual review pass. Other mixed paragraph fields remain; estimate unchanged.
|
||||
- **Improved:** Paragraph panel left/right/first-line indents and space before/after show blank when paragraphs in scope differ; uniform values still display and edits apply through `text.setFormat`. Headless regression in `panels/paragraph.rs`; estimate unchanged.
|
||||
- **Improved (#1258):** full font names (name ID 4) resolve the face instead of falling back, and Missing Fonts searches recognize the same aliases. Synthetic regressions cover loaded/cataloged fonts, localized names, real-family precedence and variable default faces. Windows font-service activation still needs reporter QA; estimate unchanged.
|
||||
- **Done:** Paragraph panel menu › Roman Hanging Punctuation (#2756), per paragraph; estimate unchanged.
|
||||
- **Done:** Paragraph panel menu › Justification… (#2755): word and letter spacing, glyph scaling (Minimum, Desired,
|
||||
|
||||
Reference in New Issue
Block a user