mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Tabs: Snap to Unit, or Shift held, puts a tab stop on the ruler's nearest tick
A tab stop placed or dragged on the Tabs panel's ruler always landed at a fractional position, and the ruler was numbered in inches with a tick every 18 pt whatever the document's units. The reference app snaps stops to the ruler units with Snap To Unit in the panel menu or with Shift held during a drag, and numbers its tab ruler in the document's units. The ruler is now numbered in the General unit with the canvas rulers' label step at the ruler's scale, and has a tick every tenth of a step. The panel menu has Snap to Unit, checked while it is on. With it on, or with Shift held, a stop placed by a click or dragged on the ruler lands on the nearest tick, through Unit::snap_to_ruler_tick as a Shift-dragged ruler guide does. A snapped click that lands on an existing stop's tick selects that stop and records no undo step. The stops still change through text.tabs.set. The reference app's page names Shift for a drag only; Shift also snapping a click, and a snapped click onto a stop's tick selecting that stop, are VectorCraft's own rules. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -2952,6 +2952,7 @@
|
||||
Snap to Pixel Прилепване към пиксел
|
||||
Precise Cursors Прецизни курсори
|
||||
Snap to Point Прилепване към точка
|
||||
Snap to Unit Прилепване към мерна единица
|
||||
Snapping Tolerance Толеранс на прилепване
|
||||
Social Square Post 1080×1080 Публикация за квадрат в социални мрежи 1080×1080
|
||||
Soft Light Мека светлина
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1905,6 +1905,7 @@ lighting Light Světlo
|
||||
Right-Justified Tab Tabulátor zarovnaný doprava
|
||||
Select a text object to set its tab stops. Vyberte textový objekt, abyste mohli nastavit jeho zarážky tabulátoru.
|
||||
Select text Vyberte text
|
||||
Snap to Unit Přichytávat k jednotkám
|
||||
+ Add Scale + Přidat měřítko
|
||||
Add Selected Artwork as One Asset Přidat vybranou kresbu jako jeden prvek
|
||||
Export Settings Nastavení exportu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1734,6 +1734,7 @@ lighting Light Lys
|
||||
Right-Justified Tab Højrejusteret tabulator
|
||||
Select a text object to set its tab stops. Markér et tekstobjekt for at angive dets tabulatorstop.
|
||||
Select text Markér tekst
|
||||
Snap to Unit Fastgør til enhed
|
||||
+ Add Scale + Tilføj skala
|
||||
Add Selected Artwork as One Asset Tilføj markeret grafik som ét aktiv
|
||||
Export Settings Eksportindstillinger
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2994,6 +2994,7 @@ effect Points Punkte
|
||||
Snap to Pixel An Pixel ausrichten
|
||||
Precise Cursors Präzise Cursor
|
||||
Snap to Point An Punkt ausrichten
|
||||
Snap to Unit An Einheit ausrichten
|
||||
Snapping Tolerance Einrasttoleranz
|
||||
Social Square Post 1080×1080 Quadratischer Social-Media-Beitrag 1080×1080
|
||||
Soft Light Weiches Licht
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1833,6 +1833,7 @@
|
||||
Snap to Grid Προσκόλληση στο πλέγμα
|
||||
Snap to Pixel Προσκόλληση σε εικονοστοιχείο
|
||||
Snap to Point Προσκόλληση σε σημείο
|
||||
Snap to Unit Προσκόλληση σε μονάδα
|
||||
Soft Light Απαλό φως
|
||||
Solid Συνεχές
|
||||
Spacing: Απόσταση:
|
||||
|
||||
|
@@ -3011,6 +3011,7 @@
|
||||
Snap to Pixel Ajustar a píxel
|
||||
Precise Cursors Cursores precisos
|
||||
Snap to Point Ajustar a punto
|
||||
Snap to Unit Ajustar a unidad
|
||||
Snapping Tolerance Tolerancia de ajuste
|
||||
Social Square Post 1080×1080 Publicación cuadrada para redes 1080×1080
|
||||
Soft Light Luz suave
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1868,6 +1868,7 @@ lighting Light Valo
|
||||
Right-Justified Tab Oikealle tasattu sarkain
|
||||
Select a text object to set its tab stops. Valitse tekstiobjekti, jonka sarkainkohdat haluat asettaa.
|
||||
Select text Valitse teksti
|
||||
Snap to Unit Kohdista yksikköön
|
||||
+ Add Scale + Lisää skaalaus
|
||||
Add Selected Artwork as One Asset Lisää valittu kuvitus yhtenä resurssina
|
||||
Export Settings Vientiasetukset
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3008,6 +3008,7 @@
|
||||
Snap to Pixel Magnétisme du pixel
|
||||
Precise Cursors Pointeurs précis
|
||||
Snap to Point Magnétisme du point
|
||||
Snap to Unit Magnétisme de l’unité
|
||||
Snapping Tolerance Tolérance du magnétisme
|
||||
Social Square Post 1080×1080 Publication carrée pour réseaux sociaux 1080×1080
|
||||
Soft Light Lumière tamisée
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3631,6 +3631,7 @@ mark-style Japanese Japán
|
||||
Snap to Pixel Illesztés pixelhez
|
||||
Precise Cursors Pontos kurzorok
|
||||
Snap to Point Illesztés pontra
|
||||
Snap to Unit Illesztés mértékegységhez
|
||||
Snapping Tolerance Illesztési tűrés
|
||||
Social Közösségi média
|
||||
Social Landscape Post 1200×630 Fekvő közösségimédia-bejegyzés 1200×630
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1734,6 +1734,7 @@ lighting Light Cahaya
|
||||
Right-Justified Tab Tab rata kanan
|
||||
Select a text object to set its tab stops. Pilih objek teks untuk mengatur tab stop-nya.
|
||||
Select text Pilih teks
|
||||
Snap to Unit Snap ke satuan
|
||||
+ Add Scale + Tambah skala
|
||||
Add Selected Artwork as One Asset Tambahkan karya seni terpilih sebagai satu aset
|
||||
Export Settings Pengaturan ekspor
|
||||
|
||||
|
@@ -3044,6 +3044,7 @@ mark-style Japanese Giapponese
|
||||
Snap to Pixel Aggancia ai pixel
|
||||
Precise Cursors Cursori di precisione
|
||||
Snap to Point Aggancia al punto
|
||||
Snap to Unit Aggancia all'unità
|
||||
Snapping Tolerance Tolleranza di aggancio
|
||||
Social Square Post 1080×1080 Post quadrato per social 1080×1080
|
||||
Soft Light Luce soffusa
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2821,6 +2821,7 @@ mark-style Japanese 日本式
|
||||
Source Image 元画像
|
||||
Image Trace View 画像トレースの表示
|
||||
Snap Options スナップオプション
|
||||
Snap to Unit 単位にスナップ
|
||||
Snapping Tolerance スナップ許容値
|
||||
Soft Light ソフトライト
|
||||
Solid 実線
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1635,6 +1635,7 @@ lighting Light სინათლე
|
||||
Right-Justified Tab მარჯვნივ სწორებული ტაბულაცია
|
||||
Select a text object to set its tab stops. ტაბულაციის გაჩერებების დასაყენებლად მონიშნეთ ტექსტური ობიექტი.
|
||||
Select text მონიშნეთ ტექსტი
|
||||
Snap to Unit ერთეულზე მიბმა
|
||||
+ Add Scale + მასშტაბის დამატება
|
||||
Add Selected Artwork as One Asset მონიშნული ნახატის დამატება ერთ რესურსად
|
||||
Export Settings ექსპორტის პარამეტრები
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2815,6 +2815,7 @@ mark-style Japanese 일본식
|
||||
Source Image 원본 이미지
|
||||
Image Trace View 이미지 추적 보기
|
||||
Snap Options 스냅 옵션
|
||||
Snap to Unit 단위에 스냅
|
||||
Snapping Tolerance 스냅 허용 오차
|
||||
Soft Light 소프트 라이트
|
||||
Solid 단색
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1748,6 +1748,7 @@ lighting Light Licht
|
||||
Right-Justified Tab Rechts uitgelijnde tabstop
|
||||
Select a text object to set its tab stops. Selecteer een tekstobject om de tabstops in te stellen.
|
||||
Select text Selecteer tekst
|
||||
Snap to Unit Uitlijnen op eenheid
|
||||
+ Add Scale + Schaal toevoegen
|
||||
Add Selected Artwork as One Asset Geselecteerde artwork als één asset toevoegen
|
||||
Export Settings Exportinstellingen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1824,6 +1824,7 @@ lighting Light Światło
|
||||
Right-Justified Tab Tabulator prawy
|
||||
Select a text object to set its tab stops. Zaznacz obiekt tekstowy, aby ustawić jego tabulatory.
|
||||
Select text Zaznacz tekst
|
||||
Snap to Unit Przyciągaj do jednostek
|
||||
+ Add Scale + Dodaj skalę
|
||||
Add Selected Artwork as One Asset Dodaj zaznaczoną kompozycję jako jeden zasób
|
||||
Export Settings Ustawienia eksportu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2501,6 +2501,7 @@
|
||||
Snap to Pixel Ajustar ao Pixel
|
||||
Precise Cursors Cursores Precisos
|
||||
Snap to Point Ajustar ao Ponto
|
||||
Snap to Unit Ajustar à Unidade
|
||||
Snapping Tolerance Tolerância de Ajuste
|
||||
Soft Light Luz Suave
|
||||
Solid Sólido
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3040,6 +3040,7 @@ mark-style Japanese Японский
|
||||
Snap to Pixel Привязать к пикселю
|
||||
Precise Cursors Точные курсоры
|
||||
Snap to Point Привязать к точке
|
||||
Snap to Unit Привязать к единицам
|
||||
Snapping Tolerance Допуск привязки
|
||||
Social Square Post 1080×1080 Квадратный пост для соцсетей 1080×1080
|
||||
Soft Light Мягкий свет
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2952,6 +2952,7 @@
|
||||
Snap to Pixel Fäst mot pixel
|
||||
Precise Cursors Exakta markörer
|
||||
Snap to Point Fäst mot punkt
|
||||
Snap to Unit Fäst mot enhet
|
||||
Snapping Tolerance Fästtolerans
|
||||
Social Square Post 1080×1080 Kvadratiskt inlägg för sociala medier 1080×1080
|
||||
Soft Light Mjukt ljus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3037,6 +3037,7 @@ effect Points Точки
|
||||
Snap to Pixel Прив’язати до пікселя
|
||||
Precise Cursors Точні курсори
|
||||
Snap to Point Прив’язати до точки
|
||||
Snap to Unit Прив’язати до одиниць
|
||||
Snapping Tolerance Допуск прив’язки
|
||||
Social Square Post 1080×1080 Квадратний пост для соцмереж 1080×1080
|
||||
Soft Light М’яке світло
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 725 and column 40.
|
@@ -3365,6 +3365,7 @@ mark-style Japanese Kiểu Nhật
|
||||
Snap to Pixel Hút vào pixel
|
||||
Precise Cursors Con trỏ chính xác
|
||||
Snap to Point Hút vào điểm
|
||||
Snap to Unit Hút vào đơn vị
|
||||
Snapping Tolerance Dung sai hút
|
||||
Social Square Post 1080×1080 Bài đăng vuông mạng xã hội 1080×1080
|
||||
Soft Mềm
|
||||
|
||||
|
@@ -2894,6 +2894,7 @@ mark-style Japanese 日式
|
||||
Snap to Pixel 对齐像素
|
||||
Precise Cursors 精确光标
|
||||
Snap to Point 对齐点
|
||||
Snap to Unit 对齐单位
|
||||
Snapping Tolerance 吸附容差
|
||||
Social Square Post 1080×1080 社交方形帖子 1080×1080
|
||||
Soft Light 柔光
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3760,6 +3760,7 @@ mark-style Japanese 日式
|
||||
Snap to Pixel 靠齊像素
|
||||
Precise Cursors 精確游標
|
||||
Snap to Point 靠齊控制點
|
||||
Snap to Unit 靠齊單位
|
||||
Snapping Tolerance 靠齊容許度
|
||||
Social Square Post 1080×1080 社群正方形貼文 1080×1080
|
||||
Soft Light 柔光
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1,5 +1,7 @@
|
||||
//! Tabs panel: tab-stop alignment, X position, leader and "align on", and a ruler on which a click
|
||||
//! adds a stop, dragging moves one and dragging it off the ruler deletes it.
|
||||
//! Tabs panel: tab-stop alignment, X position, leader and "align on", and a ruler numbered in the
|
||||
//! General unit on which a click adds a stop, dragging moves one and dragging it off the ruler
|
||||
//! deletes it. With Snap to Unit on, or Shift held, a stop placed or dragged on the ruler lands on
|
||||
//! the ruler's nearest tick.
|
||||
|
||||
use egui::{Sense, Stroke, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
@@ -175,22 +177,33 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let scale = (w as f64 - 8.0) / span;
|
||||
let to_x = |pt: f64| rect.left() + 4.0 + (pt * scale) as f32;
|
||||
let to_pt = |x: f32| (((x - rect.left() - 4.0) as f64) / scale).max(0.0);
|
||||
let mut tick = 0.0;
|
||||
while tick <= span + 1e-6 {
|
||||
let x = to_x(tick);
|
||||
let major = (tick / 36.0).round() * 36.0 == tick && ((tick / 36.0) as i64) % 2 == 0;
|
||||
p.line_segment([egui::pos2(x, rect.bottom() - if major { 10.0 } else { 5.0 }), egui::pos2(x, rect.bottom())], Stroke::new(1.0, t.text_dim));
|
||||
if major {
|
||||
// The ruler is numbered in the General unit, with the canvas rulers' label step at its scale
|
||||
// and a tick every tenth of a step.
|
||||
let unit = app.session.general_unit();
|
||||
let step = unit.ruler_step(scale);
|
||||
let minor = step / 10.0;
|
||||
for k in 0..=(unit.from_pt(span) / minor + 1e-6).floor() as usize {
|
||||
let v = k as f64 * minor;
|
||||
let x = to_x(unit.to_pt(v));
|
||||
let len = match k % 10 {
|
||||
0 => 10.0,
|
||||
5 => 7.0,
|
||||
_ => 4.0,
|
||||
};
|
||||
p.line_segment([egui::pos2(x, rect.bottom() - len), egui::pos2(x, rect.bottom())], Stroke::new(1.0, t.text_dim));
|
||||
if k % 10 == 0 {
|
||||
p.text(
|
||||
egui::pos2(x + 2.0, rect.top() + 1.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("{}", (tick / 72.0 * 100.0).round() / 100.0),
|
||||
crate::canvas::ruler_label(v, step),
|
||||
egui::FontId::proportional(9.0),
|
||||
t.text_dim,
|
||||
);
|
||||
}
|
||||
tick += 18.0;
|
||||
}
|
||||
// Snap to Unit, or Shift held: a stop placed or dragged on the ruler lands on its nearest tick.
|
||||
let snap = pstate::<bool>(ui.ctx(), "tabs-snap") || ui.input(|i| i.modifiers.shift);
|
||||
let place = |x: f32| if snap { unit.snap_to_ruler_tick(to_pt(x), scale).max(0.0) } else { to_pt(x) };
|
||||
for (i, s) in stops.iter().enumerate() {
|
||||
let x = to_x(s.position);
|
||||
let c = if i == sel { t.accent } else { t.text };
|
||||
@@ -230,7 +243,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// Dragged off the ruler: delete.
|
||||
stops.remove(i);
|
||||
} else {
|
||||
stops[i].position = to_pt(pos.x);
|
||||
stops[i].position = place(pos.x);
|
||||
stops.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||||
}
|
||||
apply(app, &stops);
|
||||
@@ -241,12 +254,18 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
match hit_at(pos.x) {
|
||||
Some(i) => set_pstate(ui.ctx(), "tabs-sel", i),
|
||||
None => {
|
||||
let position = to_pt(pos.x);
|
||||
stops.push(TabStop { position, align: ALIGNS[align_idx.min(3)].0, leader: String::new(), align_on: '.' });
|
||||
stops.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||||
let position = place(pos.x);
|
||||
// A snapped click onto a stop's tick selects that stop and records no undo step.
|
||||
let added = !stops.iter().any(|s| s.position == position);
|
||||
if added {
|
||||
stops.push(TabStop { position, align: ALIGNS[align_idx.min(3)].0, leader: String::new(), align_on: '.' });
|
||||
stops.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||||
}
|
||||
let i = stops.iter().position(|s| s.position == position).unwrap_or(0);
|
||||
set_pstate(ui.ctx(), "tabs-sel", i);
|
||||
apply(app, &stops);
|
||||
if added {
|
||||
apply(app, &stops);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +275,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some((_, para, _)) = current_para(app) else {
|
||||
ui.add_enabled(false, egui::Button::new(tl!("Select text")).frame(false));
|
||||
snap_to_unit_item(ui);
|
||||
return;
|
||||
};
|
||||
let mut stops = para.tabs;
|
||||
@@ -290,6 +310,17 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
apply(app, &stops);
|
||||
}
|
||||
snap_to_unit_item(ui);
|
||||
}
|
||||
|
||||
/// The panel menu's Snap to Unit, checked while it is on: stops placed or dragged on the ruler land
|
||||
/// on its nearest tick.
|
||||
fn snap_to_unit_item(ui: &mut Ui) {
|
||||
ui.separator();
|
||||
let on: bool = pstate(ui.ctx(), "tabs-snap");
|
||||
if menu_item(ui, tl!("Snap to Unit"), true, on) {
|
||||
set_pstate(ui.ctx(), "tabs-snap", !on);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -405,9 +436,9 @@ mod tests {
|
||||
};
|
||||
let labels = frame(&mut app, vec![]);
|
||||
let at = |label: &str| labels.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("{label} in {labels:?}")).1;
|
||||
// A label sits 2 px right of its tick: 0 and 1 inch give the ruler's scale.
|
||||
let (zero, inch) = (at("0"), at("1"));
|
||||
let to_x = |pt: f32| zero.x - 2.0 + pt * (inch.x - zero.x) / 72.0;
|
||||
// A label sits 2 px right of its tick: 0 and 100 pt give the ruler's scale.
|
||||
let (zero, hundred) = (at("0"), at("100"));
|
||||
let to_x = |pt: f32| zero.x - 2.0 + pt * (hundred.x - zero.x) / 100.0;
|
||||
let y = zero.y + 15.0;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
// Press 3 px right of the stop, then drag right; egui starts the drag 6+ px from the press.
|
||||
@@ -425,6 +456,148 @@ mod tests {
|
||||
assert!((moved_to - end.x).abs() < 1.0, "stop at {moved_to}, released at {}", end.x);
|
||||
}
|
||||
|
||||
/// One frame of the panel with `events`, Shift held when `shift` → the ruler's labels (9 pt) and
|
||||
/// where they are painted, left to right.
|
||||
fn ruler_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>, shift: bool) -> Vec<(String, egui::Pos2)> {
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(400.0, 400.0));
|
||||
let modifiers = if shift { egui::Modifiers::SHIFT } else { egui::Modifiers::NONE };
|
||||
let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect();
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
.iter()
|
||||
.filter_map(|c| match &c.shape {
|
||||
egui::Shape::Text(t) if t.galley.job.sections.first().is_some_and(|s| s.format.font_id.size == 9.0) => {
|
||||
Some((t.galley.text().to_string(), t.pos))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The labels the ruler shows in a frame of [`ruler_frame`].
|
||||
fn ruler_labels(ctx: &egui::Context, app: &mut VectorcraftApp) -> Vec<String> {
|
||||
ruler_frame(ctx, app, vec![], false).into_iter().map(|(t, _)| t).collect()
|
||||
}
|
||||
|
||||
/// Press on the ruler at `from` (points), move to `to` and release there, Shift held when
|
||||
/// `shift`. A label sits 2 px right of its tick, so the first two labels give the ruler's scale.
|
||||
fn drag_on_ruler(ctx: &egui::Context, app: &mut VectorcraftApp, from: f64, to: f64, shift: bool) {
|
||||
let labels = ruler_frame(ctx, app, vec![], shift);
|
||||
let [(zero, a), (next, b), ..] = &labels[..] else { panic!("ruler labels {labels:?}") };
|
||||
assert_eq!(zero, "0");
|
||||
let per = next.parse::<f64>().unwrap() * app.session.general_unit().points();
|
||||
let x = |pt: f64| a.x - 2.0 + (pt / per) as f32 * (b.x - a.x);
|
||||
let y = a.y + 15.0;
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let (press, end) = (egui::pos2(x(from), y), egui::pos2(x(to), y));
|
||||
ruler_frame(ctx, app, vec![egui::Event::PointerMoved(press), button(press, true)], shift);
|
||||
if press != end {
|
||||
for f in [0.25, 0.5, 1.0] {
|
||||
ruler_frame(ctx, app, vec![egui::Event::PointerMoved(press.lerp(end, f))], shift);
|
||||
}
|
||||
}
|
||||
ruler_frame(ctx, app, vec![button(end, false)], shift);
|
||||
ruler_frame(ctx, app, vec![], shift);
|
||||
}
|
||||
|
||||
/// The ruler is numbered in the General unit (the document's units), with labels at least 50 px
|
||||
/// apart as on the canvas rulers.
|
||||
#[test]
|
||||
fn ruler_is_numbered_in_the_general_unit() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(ruler_labels(&ctx, &mut app), ["0", "50", "100", "150", "200", "250", "300"]);
|
||||
app.session.execute("document.setUnits", &json!({"units": "in"})).unwrap();
|
||||
assert_eq!(ruler_labels(&ctx, &mut app), ["0", "1", "2", "3", "4"]);
|
||||
app.session.execute("document.setUnits", &json!({"units": "mm"})).unwrap();
|
||||
assert_eq!(ruler_labels(&ctx, &mut app), ["0", "25", "50", "75", "100"]);
|
||||
}
|
||||
|
||||
/// Snap to Unit, chosen from the panel menu with or without text selected, puts a stop dragged
|
||||
/// or placed by a click on the ruler on its nearest tick (every 5 pt on this points ruler).
|
||||
/// Chosen again, it turns off.
|
||||
#[test]
|
||||
fn snap_to_unit_puts_dragged_and_clicked_stops_on_the_nearest_tick() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let row = |app: &mut VectorcraftApp| {
|
||||
let rows = menu_frame(&ctx, app, vec![]);
|
||||
rows.into_iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Snap to Unit").unwrap_or_else(|| panic!("Snap to Unit"))
|
||||
};
|
||||
choose(&ctx, &mut app, "Snap to Unit");
|
||||
assert_eq!(row(&mut app).0, "✓ Snap to Unit");
|
||||
app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap();
|
||||
app.session.execute("text.tabs.set", &json!({"stops": [{"position": 50}]})).unwrap();
|
||||
assert_eq!(ruler_labels(&ctx, &mut app)[..2], ["0", "50"]);
|
||||
let positions = |app: &VectorcraftApp| current(app).unwrap().1.iter().map(|s| s.position).collect::<Vec<_>>();
|
||||
drag_on_ruler(&ctx, &mut app, 50.0, 13.4, false);
|
||||
assert_eq!(positions(&app), [15.0]);
|
||||
// A click at 33.4 pt adds a stop at 35.
|
||||
drag_on_ruler(&ctx, &mut app, 33.4, 33.4, false);
|
||||
assert_eq!(positions(&app), [15.0, 35.0]);
|
||||
assert_eq!(row(&mut app).0, "✓ Snap to Unit");
|
||||
// Chosen again, Snap to Unit turns off and the stop stays where it is dropped.
|
||||
let at = row(&mut app).1.center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
menu_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(at), button(true)]);
|
||||
menu_frame(&ctx, &mut app, vec![button(false)]);
|
||||
assert_eq!(row(&mut app).0.trim_start(), "Snap to Unit");
|
||||
drag_on_ruler(&ctx, &mut app, 35.0, 23.4, false);
|
||||
let p = positions(&app);
|
||||
assert!((p[1] - 23.4).abs() < 1.0 && p[1] != 25.0, "{p:?}");
|
||||
}
|
||||
|
||||
/// A snapped click more than 5 px from a stop, nearest to the tick that stop is on, selects the
|
||||
/// stop and records no undo step. On this millimeters ruler the ticks are 2.5 mm (7.09 pt, 12.4
|
||||
/// px) apart.
|
||||
#[test]
|
||||
fn a_snapped_click_onto_a_stop_selects_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
app.session.execute("document.setUnits", &json!({"units": "mm"})).unwrap();
|
||||
app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 224, "height": 100}})).unwrap();
|
||||
app.session.execute("text.tabs.set", &json!({"stops": [{"position": 100}]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(ruler_labels(&ctx, &mut app), ["0", "25", "50", "75"]);
|
||||
set_pstate(&ctx, "tabs-snap", true);
|
||||
let tick = 2.5 * 72.0 / 25.4;
|
||||
drag_on_ruler(&ctx, &mut app, 28.4, 28.4, false);
|
||||
let positions = |app: &VectorcraftApp| current(app).unwrap().1.iter().map(|s| s.position).collect::<Vec<_>>();
|
||||
assert_eq!(positions(&app).len(), 2);
|
||||
assert!((positions(&app)[0] - 4.0 * tick).abs() < 1e-9, "{:?}", positions(&app));
|
||||
set_pstate(&ctx, "tabs-sel", 1usize);
|
||||
let steps = app.session.active().unwrap().history.undo.len();
|
||||
// The click is 3.3 pt (5.8 px) right of the stop, past the 5 px hit tolerance and nearest its tick.
|
||||
drag_on_ruler(&ctx, &mut app, 4.0 * tick + 3.3, 4.0 * tick + 3.3, false);
|
||||
assert_eq!(positions(&app).len(), 2, "{:?}", positions(&app));
|
||||
assert_eq!(pstate::<usize>(&ctx, "tabs-sel"), 0);
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), steps, "no undo step");
|
||||
}
|
||||
|
||||
/// With Snap to Unit off, Shift held during the drag puts the stop on the ruler's nearest tick.
|
||||
#[test]
|
||||
fn shift_drag_snaps_a_stop_to_the_nearest_tick() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap();
|
||||
app.session.execute("text.tabs.set", &json!({"stops": [{"position": 50}]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let position = |app: &VectorcraftApp| current(app).unwrap().1[0].position;
|
||||
drag_on_ruler(&ctx, &mut app, 50.0, 13.4, false);
|
||||
assert!((position(&app) - 13.4).abs() < 1.0, "{}", position(&app));
|
||||
let from = position(&app);
|
||||
drag_on_ruler(&ctx, &mut app, from, 41.6, true);
|
||||
assert_eq!(position(&app), 40.0);
|
||||
// In an inches document the ticks are a tenth of an inch (7.2 pt) apart.
|
||||
app.session.execute("document.setUnits", &json!({"units": "in"})).unwrap();
|
||||
drag_on_ruler(&ctx, &mut app, 40.0, 100.0, true);
|
||||
assert!((position(&app) - 100.8).abs() < 1e-9, "{}", position(&app));
|
||||
}
|
||||
|
||||
/// One frame of the panel menu with `events` → the rows' texts and where they are painted.
|
||||
fn menu_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> Vec<(String, egui::Rect)> {
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(320.0, 400.0));
|
||||
|
||||
@@ -633,6 +633,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The frames of threaded text share one Optical Margin Alignment setting: turning it on or off in one frame sets it in every frame, threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it.
|
||||
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object.
|
||||
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
|
||||
The Tabs panel's ruler is numbered in the General unit with the canvas rulers' label steps and a tick every tenth of a step. With the panel menu's Snap to Unit on, or Shift held, a tab stop placed by a click on the ruler or dragged on it lands on the nearest tick (`text.tabs.set`).
|
||||
The Tabs panel menu's Clear All Tabs (`text.tabs.clear`), Delete Tab and Repeat Tab (`text.tabs.set`) can be chosen while the panel's ruler shows tab stops. Delete Tab removes the stop selected on the ruler. Repeat Tab replaces the stops after the selected stop with up to 20 copies of it. The copies are spaced by the selected stop's distance from the stop before it, or from the paragraph's left indent when it is the first stop, and no copy is placed at 2,000 pt or beyond. Clear All Tabs removes every stop, so the default stops every ½ inch apply again.
|
||||
- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech every menu label; Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Vietnamese, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian and Finnish also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498).
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286).
|
||||
|
||||
Reference in New Issue
Block a user