Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d372bb7f | ||
|
|
82dc5f8182 | ||
|
|
79c81d754c | ||
|
|
97318c188b | ||
|
|
94141eca82 | ||
|
|
5df4ddd4b6 |
No files matched your search
@@ -142,6 +142,17 @@ fn de_key<'de, T: Keyed, D: Deserializer<'de>>(d: D) -> std::result::Result<T, D
|
||||
T::parse(&s).ok_or_else(|| D::Error::custom(format!("`{s}`: {}", T::KEYS)))
|
||||
}
|
||||
|
||||
/// A size in pixels: a number, rounded to whole pixels (`300.0` is 300).
|
||||
fn de_pixels<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Option<u32>, D::Error> {
|
||||
let Some(v) = Option::<f64>::deserialize(d)? else { return Ok(None) };
|
||||
let px = v.round();
|
||||
if !(px.is_finite() && px >= 0.0 && px <= f64::from(u32::MAX)) {
|
||||
return Err(D::Error::custom(format!("{v} is not a size in pixels")));
|
||||
}
|
||||
// In range and whole: the cast is exact.
|
||||
Ok(Some(px as u32))
|
||||
}
|
||||
|
||||
/// The file format.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -361,8 +372,10 @@ pub struct WebSettings {
|
||||
/// JPEG: embed the sRGB profile.
|
||||
pub embed_profile: bool,
|
||||
/// The image width in pixels (the height follows).
|
||||
#[serde(deserialize_with = "de_pixels")]
|
||||
pub width: Option<u32>,
|
||||
/// The image height in pixels (the width follows).
|
||||
#[serde(deserialize_with = "de_pixels")]
|
||||
pub height: Option<u32>,
|
||||
/// The image size in percent of the art's size in points.
|
||||
pub percent: Option<f64>,
|
||||
|
||||
@@ -285,9 +285,16 @@ fn image_size_region_and_metadata() {
|
||||
assert_eq!(size(&mut s, json!({"width": 80})), (80, 60));
|
||||
assert_eq!(size(&mut s, json!({"height": 15})), (20, 15));
|
||||
assert_eq!(size(&mut s, json!({"percent": 150})), (60, 45));
|
||||
// The dialog's fields send decimal numbers: rounded to whole pixels.
|
||||
assert_eq!(size(&mut s, json!({"width": 80.0})), (80, 60));
|
||||
assert_eq!(size(&mut s, json!({"height": 14.6})), (20, 15));
|
||||
for bad in [
|
||||
json!({"percent": 0}),
|
||||
json!({"width": 0}),
|
||||
json!({"width": 0.2}),
|
||||
json!({"width": -5}),
|
||||
json!({"height": 1e300}),
|
||||
json!({"height": "tall"}),
|
||||
json!({"format": "bmp"}),
|
||||
json!({"colors": "many"}),
|
||||
json!({"matte": "plaid"}),
|
||||
|
||||
@@ -1081,47 +1081,46 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
|
||||
let step = unit.ruler_step(xf.zoom);
|
||||
let minor = step / 10.0;
|
||||
let font = egui::FontId::proportional(9.5);
|
||||
let a = xf.to_doc(top.left_top());
|
||||
let b = xf.to_doc(top.right_top());
|
||||
let clip_top = p.with_clip_rect(top);
|
||||
let mut x = (a.x / per / minor).floor() * minor;
|
||||
while x * per <= b.x {
|
||||
let sx = xf.to_screen(Point::new(x * per, 0.0)).x;
|
||||
let is_major = ((x / step).round() * step - x).abs() < minor * 0.01;
|
||||
let is_mid = ((x / (step / 2.0)).round() * (step / 2.0) - x).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
let near = |v: f64, every: f64| ((v / every).round() * every - v).abs() < minor * 0.01;
|
||||
// A tick's length: the labelled ones longest, the halves between them shorter.
|
||||
let tick = |v: f64| {
|
||||
if near(v, step) {
|
||||
RULER
|
||||
} else if is_mid {
|
||||
} else if near(v, step / 2.0) {
|
||||
7.0
|
||||
} else {
|
||||
4.0
|
||||
};
|
||||
clip_top.line_segment([pos2(sx, top.bottom() - len), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
}
|
||||
};
|
||||
// Each ruler measures along its own edge, in the canvas's coordinates turned with the view
|
||||
// (#826): at 0° the top one reads x and the left one y. `draw` gets each tick's distance
|
||||
// (screen points) from `from` along the ruler, and its value in `unit`.
|
||||
let ticks = |from: Pos2, to: Pos2, axis: vectorcraft_geom::Vec2, draw: &mut dyn FnMut(f32, f64)| {
|
||||
let along = |s: Pos2| xf.to_doc(s).to_vec2().dot(axis);
|
||||
let (a, b) = (along(from), along(to));
|
||||
let mut v = (a / per / minor).floor() * minor;
|
||||
while v * per <= b {
|
||||
draw(((v * per - a) * xf.zoom) as f32, v);
|
||||
v += minor;
|
||||
}
|
||||
};
|
||||
// The canvas's directions along the screen's x and y.
|
||||
let (sn, cs) = xf.rot.sin_cos();
|
||||
let clip_top = p.with_clip_rect(top);
|
||||
ticks(top.left_top(), top.right_top(), vectorcraft_geom::Vec2::new(cs, -sn), &mut |d, x| {
|
||||
let sx = top.left() + d;
|
||||
clip_top.line_segment([pos2(sx, top.bottom() - tick(x)), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
|
||||
if near(x, step) {
|
||||
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, ruler_label(x, step), font.clone(), t.ruler_tick);
|
||||
}
|
||||
x += minor;
|
||||
}
|
||||
let a = xf.to_doc(left.left_top());
|
||||
let b = xf.to_doc(left.left_bottom());
|
||||
});
|
||||
let clip_left = p.with_clip_rect(left);
|
||||
let mut y = (a.y / per / minor).floor() * minor;
|
||||
while y * per <= b.y {
|
||||
let sy = xf.to_screen(Point::new(0.0, y * per)).y;
|
||||
let is_major = ((y / step).round() * step - y).abs() < minor * 0.01;
|
||||
let is_mid = ((y / (step / 2.0)).round() * (step / 2.0) - y).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
RULER
|
||||
} else if is_mid {
|
||||
7.0
|
||||
} else {
|
||||
4.0
|
||||
};
|
||||
clip_left.line_segment([pos2(left.right() - len, sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
ticks(left.left_top(), left.left_bottom(), vectorcraft_geom::Vec2::new(sn, cs), &mut |d, y| {
|
||||
let sy = left.top() + d;
|
||||
clip_left.line_segment([pos2(left.right() - tick(y), sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
|
||||
if near(y, step) {
|
||||
// Vertical labels read top-to-bottom, one digit per line like Illustrator.
|
||||
let s = ruler_label(y, step);
|
||||
for (k, ch) in s.chars().enumerate() {
|
||||
for (k, ch) in ruler_label(y, step).chars().enumerate() {
|
||||
clip_left.text(
|
||||
pos2(left.left() + 4.0, sy + 2.0 + k as f32 * 8.5),
|
||||
egui::Align2::LEFT_TOP,
|
||||
@@ -1131,8 +1130,7 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
|
||||
);
|
||||
}
|
||||
}
|
||||
y += minor;
|
||||
}
|
||||
});
|
||||
if let Some(h) = hover {
|
||||
let s = xf.to_screen(h);
|
||||
clip_top.line_segment([pos2(s.x, top.top()), pos2(s.x, top.bottom())], Stroke::new(1.0, t.text));
|
||||
@@ -2053,6 +2051,47 @@ mod tests {
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_geom::Shape as _;
|
||||
|
||||
/// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one
|
||||
/// reads x where the canvas has it.
|
||||
#[test]
|
||||
fn rulers_cover_their_length_at_any_rotation() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let full = egui::Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0));
|
||||
let [top, left, _] = ruler_rects(full);
|
||||
for deg in [0.0_f64, 30.0, 90.0, 135.0, 200.0] {
|
||||
let xf = Xf { rect: full, zoom: 1.0, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, Unit::Points, &Tokens::get(ui.ctx())));
|
||||
out.textures_delta.clear();
|
||||
// The ticks' positions along each ruler.
|
||||
let along = |ruler: egui::Rect, x: bool| -> Vec<f32> {
|
||||
let mut at: Vec<f32> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|c| c.clip_rect == ruler)
|
||||
.filter_map(|c| match &c.shape {
|
||||
Shape::LineSegment { points: [a, b], .. } if x && a.x == b.x => Some(a.x),
|
||||
Shape::LineSegment { points: [a, b], .. } if !x && a.y == b.y => Some(a.y),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
at.sort_by(f32::total_cmp);
|
||||
at
|
||||
};
|
||||
for (ruler, x) in [(top, true), (left, false)] {
|
||||
let at = along(ruler, x);
|
||||
let (start, end) = if x { (ruler.left(), ruler.right()) } else { (ruler.top(), ruler.bottom()) };
|
||||
let (Some(first), Some(last)) = (at.first(), at.last()) else { panic!("{deg}°: no ticks") };
|
||||
assert!(first - start < 20.0 && end - last < 20.0, "{deg}°: ticks from {first} to {last} on {start}..{end}");
|
||||
assert!(at.windows(2).all(|w| w[1] - w[0] < 20.0), "{deg}°: a gap in the ticks");
|
||||
}
|
||||
if deg == 0.0 {
|
||||
let origin = xf.to_screen(Point::ZERO).x;
|
||||
assert!(along(top, true).iter().any(|x| (x - origin).abs() < 0.01), "a tick at x = 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shaper_selection_highlights_only_the_visible_result() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -265,23 +265,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Align buttons.
|
||||
for (icon, tip, p) in [
|
||||
("align-start-vertical", "Horizontal Align Left", json!({"horizontal": "left"})),
|
||||
("align-center-vertical", "Horizontal Align Center", json!({"horizontal": "center"})),
|
||||
("align-end-vertical", "Horizontal Align Right", json!({"horizontal": "right"})),
|
||||
("align-start-horizontal", "Vertical Align Top", json!({"vertical": "top"})),
|
||||
("align-center-horizontal", "Vertical Align Center", json!({"vertical": "center"})),
|
||||
("align-end-horizontal", "Vertical Align Bottom", json!({"vertical": "bottom"})),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tl!(tip), false, 24.0).clicked() {
|
||||
let mut p = p;
|
||||
if sel.len() == 1 {
|
||||
p["to"] = json!("artboard");
|
||||
}
|
||||
app.run("object.align", p).ok();
|
||||
}
|
||||
}
|
||||
crate::panels::align::align_buttons(app, ui, 24.0);
|
||||
ui.separator();
|
||||
// The Transform link opens the whole Transform panel (reference point, rotate,
|
||||
// shear, options) in a popover; X/Y/W/H follow inline while the bar has room.
|
||||
|
||||
@@ -90,12 +90,19 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let screen =
|
||||
d.fields.get("screen").and_then(Value::as_array).and_then(|a| Some(egui::pos2(a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32)));
|
||||
let chip = Point::new(d.f64("x", 0.0), d.f64("y", 0.0));
|
||||
let pos = screen.unwrap_or_else(|| {
|
||||
app.canvas_rect.zip(app.view().copied()).map_or(ctx.content_rect().center(), |(r, v)| Xf::new(r, &v).to_screen(chip) + vec2(14.0, 14.0))
|
||||
let area = egui::Id::new("gradient-stop-popover");
|
||||
let size = ctx.memory(|m| m.area_rect(area)).map_or(POPOVER_SIZE, |r| r.size());
|
||||
let pos = screen.unwrap_or_else(|| match app.canvas_rect.zip(app.view().copied()) {
|
||||
Some((canvas, v)) => {
|
||||
let xf = Xf::new(canvas, &v);
|
||||
let at = xf.to_screen(chip);
|
||||
selection_on_screen(app, &xf).map_or(at + vec2(14.0, 14.0), |art| beside_art(at, art, size, canvas))
|
||||
}
|
||||
None => ctx.content_rect().center(),
|
||||
});
|
||||
let swatches = d.str("tab") == "swatches";
|
||||
let mut tab = None;
|
||||
let resp = egui::Area::new(egui::Id::new("gradient-stop-popover"))
|
||||
let resp = egui::Area::new(area)
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.constrain(true)
|
||||
@@ -150,6 +157,34 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The popover's size before it has been drawn.
|
||||
const POPOVER_SIZE: egui::Vec2 = vec2(272.0, 360.0);
|
||||
|
||||
/// The selected art's box on the screen.
|
||||
fn selection_on_screen(app: &VectorcraftApp, xf: &Xf) -> Option<egui::Rect> {
|
||||
let st = app.session.active()?;
|
||||
let b = st.doc.bounds_of(&st.selection.objects, false)?;
|
||||
Some(egui::Rect::from_points(&xf.quad(b)))
|
||||
}
|
||||
|
||||
/// Where the popover opened from a stop's chip at `chip` goes: beside the selected `art` on the
|
||||
/// first side of it with room for a popover of `size` in `room` (right, left, below, above), so
|
||||
/// the art it recolours stays in view (#862); else beside the chip.
|
||||
fn beside_art(chip: egui::Pos2, art: egui::Rect, size: egui::Vec2, room: egui::Rect) -> egui::Pos2 {
|
||||
const GAP: f32 = 12.0;
|
||||
let x = (chip.x - size.x / 2.0).clamp(room.left(), (room.right() - size.x).max(room.left()));
|
||||
let y = (chip.y - size.y / 2.0).clamp(room.top(), (room.bottom() - size.y).max(room.top()));
|
||||
[
|
||||
egui::pos2(art.right() + GAP, y),
|
||||
egui::pos2(art.left() - GAP - size.x, y),
|
||||
egui::pos2(x, art.bottom() + GAP),
|
||||
egui::pos2(x, art.top() - GAP - size.y),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|p| room.contains_rect(egui::Rect::from_min_size(*p, size)))
|
||||
.unwrap_or(chip + vec2(14.0, 14.0))
|
||||
}
|
||||
|
||||
/// Document swatches as tiles: a click gives the stop (or point) that colour.
|
||||
fn swatch_grid(app: &mut VectorcraftApp, ui: &mut egui::Ui, target: &Target) {
|
||||
let colors = swatch_colors(app);
|
||||
@@ -248,6 +283,28 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
/// #862: the popover goes beside the art, on the first side with room, not over it.
|
||||
#[test]
|
||||
fn the_popover_goes_beside_the_art_it_edits() {
|
||||
let room = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(1200.0, 800.0));
|
||||
let size = egui::vec2(272.0, 360.0);
|
||||
let place = |art: egui::Rect| egui::Rect::from_min_size(beside_art(art.center(), art, size, room), size);
|
||||
// Room on the right.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(300.0, 300.0), egui::pos2(500.0, 450.0));
|
||||
assert!(place(art).left() > art.right() && room.contains_rect(place(art)));
|
||||
// Against the right edge: on the left.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(900.0, 300.0), egui::pos2(1150.0, 450.0));
|
||||
assert!(place(art).right() < art.left());
|
||||
// As wide as the room: below it, or above it near the bottom.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(100.0, 20.0), egui::pos2(1100.0, 300.0));
|
||||
assert!(place(art).top() > art.bottom());
|
||||
let art = egui::Rect::from_min_max(egui::pos2(100.0, 420.0), egui::pos2(1100.0, 780.0));
|
||||
assert!(place(art).bottom() < art.top());
|
||||
// Nowhere to go: beside the chip, as before.
|
||||
let art = room.shrink(10.0);
|
||||
assert_eq!(beside_art(art.center(), art, size, room), art.center() + egui::vec2(14.0, 14.0));
|
||||
}
|
||||
|
||||
fn stops(app: &VectorcraftApp) -> Vec<GradientStop> {
|
||||
match selected(app) {
|
||||
Some(Target::Stop(stops, _)) => stops,
|
||||
|
||||
@@ -1497,6 +1497,8 @@
|
||||
Monospaced Monoespaciada
|
||||
More Más
|
||||
More Pathfinder options Más opciones de Buscatrazos
|
||||
More Align options Más opciones de alineación
|
||||
More Paragraph options Más opciones de párrafo
|
||||
More Settings Más ajustes
|
||||
Move Mover
|
||||
Move All Objects Mover todos los objetos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1496,6 +1496,8 @@
|
||||
Monospaced À chasse fixe
|
||||
More Plus
|
||||
More Pathfinder options Plus d’options Pathfinder
|
||||
More Align options Plus d’options d’alignement
|
||||
More Paragraph options Plus d’options de paragraphe
|
||||
More Settings Plus de réglages
|
||||
Move Déplacer
|
||||
Move All Objects Déplacer tous les objets
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1497,6 +1497,8 @@
|
||||
Monospaced Monospaziato
|
||||
More Altro
|
||||
More Pathfinder options Altre opzioni di Elaborazione tracciati
|
||||
More Align options Altre opzioni di allineamento
|
||||
More Paragraph options Altre opzioni di paragrafo
|
||||
More Settings Altre impostazioni
|
||||
Move Sposta
|
||||
Move All Objects Sposta tutti gli oggetti
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1639,6 +1639,8 @@
|
||||
Monochrome images モノクロイメージ
|
||||
More 多く
|
||||
More Pathfinder options その他のパスファインダーオプション
|
||||
More Align options その他の整列オプション
|
||||
More Paragraph options その他の段落オプション
|
||||
More Settings その他の設定
|
||||
Move 移動
|
||||
Move Anchors アンカーポイントを移動
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1313,6 +1313,8 @@
|
||||
Monochrome images Imagens monocromáticas
|
||||
More Mais
|
||||
More Pathfinder options Mais opções de Localizador de Caminhos
|
||||
More Align options Mais opções de alinhamento
|
||||
More Paragraph options Mais opções de parágrafo
|
||||
More Settings Mais Configurações
|
||||
Move Mover
|
||||
Move Anchors Mover Âncoras
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1495,6 +1495,8 @@
|
||||
Monospaced Моноширинный
|
||||
More Больше
|
||||
More Pathfinder options Дополнительные параметры обработки контуров
|
||||
More Align options Дополнительные параметры выравнивания
|
||||
More Paragraph options Дополнительные параметры абзаца
|
||||
More Settings Дополнительные настройки
|
||||
Move Переместить
|
||||
Move All Objects Переместить все объекты
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1491,6 +1491,8 @@
|
||||
Monospaced Моноширинний
|
||||
More Більше
|
||||
More Pathfinder options Додаткові параметри обробки контурів
|
||||
More Align options Додаткові параметри вирівнювання
|
||||
More Paragraph options Додаткові параметри абзацу
|
||||
More Settings Додаткові налаштування
|
||||
Move Перемістити
|
||||
Move All Objects Перемістити всі об’єкти
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 632 and column 40.
|
@@ -1455,6 +1455,8 @@
|
||||
Monochrome images 单色图像
|
||||
More 较多
|
||||
More Pathfinder options 更多路径查找器选项
|
||||
More Align options 更多对齐选项
|
||||
More Paragraph options 更多段落选项
|
||||
More Settings 更多设置
|
||||
Move 移动
|
||||
Move All Objects 移动所有对象
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1497,6 +1497,8 @@
|
||||
Monospaced 等寬
|
||||
More 更多
|
||||
More Pathfinder options 更多路徑管理員選項
|
||||
More Align options 更多對齊選項
|
||||
More Paragraph options 更多段落選項
|
||||
More Settings 更多設定
|
||||
Move 移動
|
||||
Move All Objects 移動所有物件
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -52,6 +52,26 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [
|
||||
("dc-dist-right", "Horizontal Distribute Right", "horizontal", "right"),
|
||||
];
|
||||
|
||||
/// The six align buttons of the Control bar and the Properties panel, `size` points square.
|
||||
/// They align to what the Align panel's Align To says (the key object while there is one), as
|
||||
/// its own buttons do (#789).
|
||||
pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
||||
let n = selection_len(app);
|
||||
let to = align_to(app, ui.ctx());
|
||||
for (icon, tip, axis, v) in [
|
||||
("align-start-vertical", "Horizontal Align Left", "horizontal", "left"),
|
||||
("align-center-vertical", "Horizontal Align Center", "horizontal", "center"),
|
||||
("align-end-vertical", "Horizontal Align Right", "horizontal", "right"),
|
||||
("align-start-horizontal", "Vertical Align Top", "vertical", "top"),
|
||||
("align-center-horizontal", "Vertical Align Center", "vertical", "center"),
|
||||
("align-end-horizontal", "Vertical Align Bottom", "vertical", "bottom"),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() {
|
||||
app.run("object.align", align_params(json!({axis: v}), to, n)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the selection has a key object.
|
||||
fn has_key(app: &VectorcraftApp) -> bool {
|
||||
app.session.active().is_some_and(|st| st.selection.key.is_some())
|
||||
@@ -183,4 +203,35 @@ mod tests {
|
||||
app.run("select.key", json!({})).unwrap();
|
||||
assert_eq!(align_to(&app, &ctx), AlignTo::Selection, "no key: the choice again");
|
||||
}
|
||||
|
||||
/// #789: the Control bar's and the Properties panel's align buttons follow Align To too.
|
||||
#[test]
|
||||
fn the_other_align_buttons_align_to_the_align_to_choice() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap()["id"].clone();
|
||||
let b = app.run("shape.rectangle", json!({"x": 80, "y": 50, "width": 30, "height": 30})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
set_pstate(&ctx, "align-to", AlignTo::Artboard);
|
||||
// One frame finds the first button (Horizontal Align Right is third); the next clicks it.
|
||||
let at = std::cell::Cell::new(egui::Pos2::ZERO);
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
at.set(ui.cursor().min);
|
||||
align_buttons(app, ui, 24.0);
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(&mut app, vec![]);
|
||||
let right = at.get() + egui::vec2(2.0 * (24.0 + ctx.global_style().spacing.item_spacing.x) + 12.0, 12.0);
|
||||
let press = |pressed| egui::Event::PointerButton { pos: right, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(right), press(true)]);
|
||||
frame(&mut app, vec![press(false)]);
|
||||
let st = app.session.active().unwrap();
|
||||
let x1 = |id: &serde_json::Value| st.doc.bounds_of(&[vectorcraft_doc::NodeId(id.as_u64().unwrap())], false).unwrap().x1;
|
||||
assert_eq!((x1(&a), x1(&b)), (200.0, 200.0), "both to the artboard's right edge");
|
||||
}
|
||||
}
|
||||
@@ -496,7 +496,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
(Some(c), _) => components(mode, &c),
|
||||
(None, _) => vec![0.0; mode.labels().len()],
|
||||
};
|
||||
super::recent_colors_row(app, ui);
|
||||
let recent = super::recent_colors_row(app, ui);
|
||||
widgets::divider(ui);
|
||||
// An edit of the active colour with its displayed components (`new`), or a colour clicked with
|
||||
// Alt held for the inactive proxy (`behind`).
|
||||
@@ -585,6 +585,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
*new = Some((c, phase, components(mode, &c)));
|
||||
}
|
||||
};
|
||||
// A recent colour, like the chips, recolours the selected gradient stop (or point).
|
||||
if let Some(c) = recent {
|
||||
pick(c, Live::Released, &mut new);
|
||||
}
|
||||
// None / Black / White chips and the hex field.
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
@@ -691,12 +695,23 @@ mod tests {
|
||||
/// Run the panel for one frame with `events` and `modifiers`; returns the spectrum's rect (when
|
||||
/// the options are shown) and the panel's height.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>, modifiers: egui::Modifiers) -> (Option<Rect>, f32) {
|
||||
frame_with(app, ctx, events, modifiers, "color-spectrum")
|
||||
}
|
||||
|
||||
/// [`frame`] returning the rect of the panel's widget `id` instead of the spectrum's.
|
||||
fn frame_with(
|
||||
app: &mut VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
events: Vec<egui::Event>,
|
||||
modifiers: egui::Modifiers,
|
||||
id: impl std::hash::Hash + std::fmt::Debug,
|
||||
) -> (Option<Rect>, f32) {
|
||||
let mut out = (None, 0.0);
|
||||
let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect();
|
||||
let input = egui::RawInput { events, ..Default::default() };
|
||||
let mut full = ctx.run_ui(input, |ui| {
|
||||
show(app, ui);
|
||||
out = (ctx.read_response(ui.id().with("color-spectrum")).map(|r| r.rect), ui.min_rect().height());
|
||||
out = (ctx.read_response(ui.id().with(&id)).map(|r| r.rect), ui.min_rect().height());
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
out
|
||||
@@ -712,6 +727,27 @@ mod tests {
|
||||
frame(app, ctx, vec![], egui::Modifiers::NONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recent_colour_recolours_the_selected_gradient_stop() {
|
||||
// #835: it replaced the whole gradient with the colour.
|
||||
let mut app = app();
|
||||
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
|
||||
let stops = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]);
|
||||
app.run("paint.setFill", json!({"gradient": {"stops": stops, "start": [10, 35], "end": [60, 35]}})).unwrap();
|
||||
app.select_tool("gradient");
|
||||
app.run("gradient.selectStop", json!({"index": 1})).unwrap();
|
||||
let red = app.session.recent_colors.iter().position(|c| c.to_hex() == "#ff0000").expect("red is a recent colour");
|
||||
let ctx = egui::Context::default();
|
||||
let none = egui::Modifiers::NONE;
|
||||
let chip = frame_with(&mut app, &ctx, vec![], none, ("recent", red)).0.expect("the chip is drawn").center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos: chip, button: egui::PointerButton::Primary, pressed, modifiers: none };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(chip), button(true)], none);
|
||||
frame(&mut app, &ctx, vec![button(false)], none);
|
||||
let Paint::Gradient(g) = crate::panels::current_paints(&app).0 else { panic!("still a gradient") };
|
||||
let hex: Vec<String> = g.gradient.stops.iter().map(|s| s.color.to_hex()).collect();
|
||||
assert_eq!(hex, ["#ffffff", "#ff0000"]);
|
||||
}
|
||||
|
||||
fn paints_hex(app: &VectorcraftApp) -> (String, String) {
|
||||
let (f, s) = crate::panels::current_paints(app);
|
||||
(f.color().map(|c| c.to_hex()).unwrap_or_default(), s.color().map(|c| c.to_hex()).unwrap_or_default())
|
||||
|
||||
@@ -412,8 +412,8 @@ pub(crate) fn label_or_name(s: &str, built_in: bool) -> &str {
|
||||
}
|
||||
|
||||
/// "Recent Colors" header + a row of chips (the Session's recent colours, which every paint
|
||||
/// command feeds); clicking one applies it to the active proxy (Alt: the inactive one).
|
||||
pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// command feeds); returns the one clicked, for the caller to apply.
|
||||
pub(crate) fn recent_colors_row(app: &VectorcraftApp, ui: &mut Ui) -> Option<Color> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
crate::widgets::subheader(ui, "Recent Colors");
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::hover());
|
||||
@@ -430,9 +430,7 @@ pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
chosen = Some(*c);
|
||||
}
|
||||
}
|
||||
if let Some(c) = chosen {
|
||||
apply_click(app, ui, json!({"color": color_json(&c)}));
|
||||
}
|
||||
chosen
|
||||
}
|
||||
|
||||
/// A colour as command JSON, keeping its model.
|
||||
|
||||
@@ -60,6 +60,24 @@ fn shown_alignment(justify: Justify, rtl: bool) -> Justify {
|
||||
}
|
||||
}
|
||||
|
||||
/// The seven alignment buttons, `size` points square, the one of `para`'s alignment on (the
|
||||
/// Paragraph panel's and the Properties panel's).
|
||||
pub fn alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32) {
|
||||
let shown = shown_alignment(para.justify, resolved_rtl(app, para));
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, size).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Hyphenate checkbox of `para`'s paragraphs.
|
||||
pub fn hyphenate_check(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle) {
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some((_, para)) = text_style(app) else {
|
||||
@@ -67,14 +85,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
};
|
||||
let rtl = resolved_rtl(app, ¶);
|
||||
let shown = shown_alignment(para.justify, rtl);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, 28.0).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
alignment_buttons(app, ui, ¶, 28.0);
|
||||
// Paragraph direction, with the Middle Eastern (Indic) options.
|
||||
if app.session.prefs.show_indic_options {
|
||||
ui.add_space(6.0);
|
||||
@@ -127,9 +140,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if pstate::<bool>(ui.ctx(), "pa-hide-options") {
|
||||
return;
|
||||
}
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
hyphenate_check(app, ui, ¶);
|
||||
// Japanese composition: how punctuation is spaced (JLREQ 3.1), with the East Asian options.
|
||||
if app.session.prefs.show_east_asian_options {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -57,21 +57,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Align"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, tip, p) in [
|
||||
("align-start-vertical", tl!("Horizontal Align Left"), json!({"horizontal": "left"})),
|
||||
("align-center-vertical", tl!("Horizontal Align Center"), json!({"horizontal": "center"})),
|
||||
("align-end-vertical", tl!("Horizontal Align Right"), json!({"horizontal": "right"})),
|
||||
("align-start-horizontal", tl!("Vertical Align Top"), json!({"vertical": "top"})),
|
||||
("align-center-horizontal", tl!("Vertical Align Center"), json!({"vertical": "center"})),
|
||||
("align-end-horizontal", tl!("Vertical Align Bottom"), json!({"vertical": "bottom"})),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tip, false, 26.0).clicked() {
|
||||
let mut p = p;
|
||||
if n_sel == 1 {
|
||||
p["to"] = json!("artboard");
|
||||
}
|
||||
app.run("object.align", p).ok();
|
||||
}
|
||||
super::align::align_buttons(app, ui, 26.0);
|
||||
// The rest (Distribute Objects, Distribute Spacing, Align To) is the Align panel's.
|
||||
if widgets::icon_button(ui, "ellipsis", tl!("More Align options"), false, 26.0).clicked() {
|
||||
app.ui.open_panel = Some("align".into());
|
||||
}
|
||||
});
|
||||
if n_sel > 1 {
|
||||
@@ -534,13 +523,17 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
super::character::font_pickers(app, ui, &s, ("font", "font-style"), (w - 4.0, w - 4.0));
|
||||
super::character::metrics_grid(app, ui, "props-char", &s, w, false);
|
||||
section_header(ui, tl!("Paragraph"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, j) in [("align-start-vertical", "left"), ("align-center-vertical", "center"), ("align-end-vertical", "right")] {
|
||||
if widgets::icon_button(ui, icon, j, false, 24.0).clicked() {
|
||||
app.run("text.setStyle", json!({"justify": j})).ok();
|
||||
// As in the Paragraph panel: the seven alignments and Hyphenate (#775); the rest is there.
|
||||
if let Some((_, para)) = super::character::text_style(app) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
super::paragraph::alignment_buttons(app, ui, ¶, 24.0);
|
||||
if widgets::icon_button(ui, "ellipsis", tl!("More Paragraph options"), false, 24.0).clicked() {
|
||||
app.ui.open_panel = Some("paragraph".into());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
super::paragraph::hyphenate_check(app, ui, ¶);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -603,6 +596,20 @@ mod tests {
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
/// #775: type's Paragraph section has Hyphenate, and its checkbox turns it on.
|
||||
#[test]
|
||||
fn type_paragraph_section_hyphenates() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
app.run("text.create", json!({"x": 10, "y": 50, "text": "Paragraph"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Hyphenate");
|
||||
let hyphenate = |app: &VectorcraftApp| crate::panels::character::text_style(app).unwrap().1.hyphenate;
|
||||
assert!(hyphenate(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_artboards_adds_artboards_and_exits() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
@@ -642,7 +642,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
super::recent_colors_row(app, ui);
|
||||
// A recent colour paints the active proxy (Alt: the inactive one), as a swatch does.
|
||||
if let Some(c) = super::recent_colors_row(app, ui) {
|
||||
super::apply_click(app, ui, json!({"color": super::color_json(&c)}));
|
||||
}
|
||||
widgets::divider(ui);
|
||||
widgets::subheader(ui, tl!("Swatch Tiles"));
|
||||
let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), tl!("Find")) } else { String::new() };
|
||||
|
||||
Reference in new issue
Block a user