Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c81d754c | ||
|
|
97318c188b | ||
|
|
94141eca82 |
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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