mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
879 lines
44 KiB
Rust
879 lines
44 KiB
Rust
//! The document canvas: rendering, rulers, navigation, pointer routing to tools and on-canvas
|
||
//! selection visuals (bounding box, anchors, handles, smart-guide style labels).
|
||
|
||
use egui::emath::GuiRounding as _;
|
||
use egui::{Color32, CornerRadius, Pos2, Sense, Shape, Stroke, StrokeKind, Ui, Vec2, pos2, vec2};
|
||
use serde_json::json;
|
||
use vectorcraft_doc::text::TextKind;
|
||
use vectorcraft_doc::{Node, NodeKind, SymbolBoxes, Unit};
|
||
use vectorcraft_geom::{Affine, BezPath, PathEl, Point, Rect};
|
||
use vectorcraft_tools::{Cursor, Mods, Overlay, PenPose, PointerEvent, PointerKind};
|
||
|
||
use crate::opening;
|
||
use crate::state::View;
|
||
use crate::stylus::StylusFeed;
|
||
use crate::theme::{self, Tokens};
|
||
use crate::{CacheKey, VectorcraftApp, now_ms, widgets};
|
||
|
||
mod artboards;
|
||
mod context;
|
||
mod cursor;
|
||
mod drag;
|
||
mod fill;
|
||
mod grid;
|
||
mod home;
|
||
mod input;
|
||
mod outline;
|
||
mod overlays;
|
||
mod panes;
|
||
mod raster;
|
||
mod task_bar;
|
||
mod tool_overlays;
|
||
mod view;
|
||
|
||
use artboards::{ArtboardNameEdit, artboard_label, artboard_marks, artboard_name_editor, name_edit_id};
|
||
#[cfg(test)]
|
||
use artboards::{artboard_name_field_rect, begin_artboard_name_edit, commit_artboard_name, names_an_artboard, tool_name_rect};
|
||
use context::{context_menu, panel_drop};
|
||
pub use cursor::{HOVER_STILL_SECS, STILL_SECS, ViewKey};
|
||
use cursor::{cursor_icon, hover_cursor, hover_wait, still_wait};
|
||
use drag::drag_id;
|
||
pub use drag::{apply_requests, dispatch, mods};
|
||
pub(crate) use drag::{end_document_drag, pose_on_page, release_art_in_window, release_over_panel};
|
||
#[cfg(test)]
|
||
use drag::{frame_moves, tool_drag_doc_id, tool_drag_moved, tool_frame_moves, tool_press_doc_id};
|
||
pub use fill::Fill;
|
||
use fill::fill_uncovered;
|
||
#[cfg(test)]
|
||
use fill::{fill_view, polygon_area};
|
||
#[cfg(test)]
|
||
use grid::ruler_rects;
|
||
use grid::{LineLook, PIXEL_GRID, PIXEL_GRID_ZOOM, checker, clip_segment, grid, ruler_input, rulers, textured_quad, view_bounds};
|
||
pub(crate) use grid::{RulerScale, checker_texture, ruler_label, ruler_origin_dragging};
|
||
#[cfg(test)]
|
||
pub(crate) use grid::{TRANSPARENCY_GRID, vertical_label_height};
|
||
use home::home;
|
||
use input::handle_input;
|
||
use outline::KEY_OUTLINE;
|
||
#[cfg(test)]
|
||
use outline::{OVERLAY_MAX_ANCHORS, c32, node_outline, walk_drawn};
|
||
#[cfg(test)]
|
||
use overlays::selected_anchor;
|
||
use overlays::{HandleLook, draw_inside_overlay, draw_overlays, hover_highlight, ime_geometry, selection_overlay};
|
||
pub use panes::Panes;
|
||
pub(crate) use panes::{consolidate, tile, tiled_area};
|
||
use raster::{changed_pixels, pixel_region, upload};
|
||
pub(crate) use raster::{color_image, color_image_owned};
|
||
use task_bar::task_bar;
|
||
pub(crate) use task_bar::{has_colors, task_bar_rect};
|
||
pub use tool_overlays::HiddenChars;
|
||
#[cfg(test)]
|
||
use tool_overlays::{DIAL_RADIUS, HiddenMarks};
|
||
use tool_overlays::{
|
||
grid_repeat_overlay, hidden_chars_overlay, live_paint_gap_overlays, mirror_axis_overlay, original_path_overlay, print_tiling_overlay,
|
||
rotate_view_dial, shaper_widget_overlay, slice_overlay, thread_overlays,
|
||
};
|
||
pub(crate) use view::animated_zoom;
|
||
pub use view::fit;
|
||
use view::scroll_bars;
|
||
#[cfg(test)]
|
||
use view::{ScrollRest, Span, scroll_by, scroll_spans, scroll_tracks};
|
||
|
||
const RULER: f32 = 16.0;
|
||
/// The scroll bars' thickness along the window's right and bottom edges.
|
||
const SCROLL_BAR: f32 = 12.0;
|
||
|
||
/// Screen ↔ document mapping for one frame.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Xf {
|
||
pub rect: egui::Rect,
|
||
pub zoom: f64,
|
||
pub center: Point,
|
||
/// View rotation in radians (Rotate View), clockwise on screen.
|
||
pub rot: f64,
|
||
}
|
||
|
||
impl Xf {
|
||
pub fn new(rect: egui::Rect, v: &View) -> Self {
|
||
Self { rect, zoom: v.zoom, center: v.center, rot: v.rotation.to_radians() }
|
||
}
|
||
pub fn to_screen(&self, p: Point) -> Pos2 {
|
||
let c = self.rect.center();
|
||
let (dx, dy) = ((p.x - self.center.x) * self.zoom, (p.y - self.center.y) * self.zoom);
|
||
let (sn, cs) = self.rot.sin_cos();
|
||
pos2(c.x + (dx * cs - dy * sn) as f32, c.y + (dx * sn + dy * cs) as f32)
|
||
}
|
||
pub fn to_doc(&self, p: Pos2) -> Point {
|
||
let c = self.rect.center();
|
||
let (dx, dy) = ((p.x - c.x) as f64, (p.y - c.y) as f64);
|
||
let (sn, cs) = self.rot.sin_cos();
|
||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||
Point::new(self.center.x + ux / self.zoom, self.center.y + uy / self.zoom)
|
||
}
|
||
/// Screen-space corners of a document rect (a rotated quad when the view is rotated).
|
||
pub fn quad(&self, r: Rect) -> Vec<Pos2> {
|
||
[Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].iter().map(|p| self.to_screen(*p)).collect()
|
||
}
|
||
/// Convert a screen-space delta to a document delta.
|
||
pub fn delta_to_doc(&self, d: egui::Vec2) -> vectorcraft_geom::Vec2 {
|
||
let (sn, cs) = self.rot.sin_cos();
|
||
let (dx, dy) = (d.x as f64, d.y as f64);
|
||
vectorcraft_geom::Vec2::new((dx * cs + dy * sn) / self.zoom, (-dx * sn + dy * cs) / self.zoom)
|
||
}
|
||
pub fn rect_to_screen(&self, r: Rect) -> egui::Rect {
|
||
egui::Rect::from_two_pos(self.to_screen(Point::new(r.x0, r.y0)), self.to_screen(Point::new(r.x1, r.y1)))
|
||
}
|
||
/// Affine mapping document points to screen points.
|
||
pub fn affine(&self) -> Affine {
|
||
let c = self.rect.center();
|
||
Affine::translate((c.x as f64, c.y as f64)) * Affine::rotate(self.rot) * Affine::scale(self.zoom) * Affine::translate(-self.center.to_vec2())
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
enum Drag {
|
||
Tool,
|
||
/// Hand tool / Space drag, or a middle-button drag with any tool (`middle`).
|
||
Pan {
|
||
start: Pos2,
|
||
center: Point,
|
||
middle: bool,
|
||
},
|
||
/// A Zoom tool drag without Animated Zoom: the area to zoom to.
|
||
ZoomBox {
|
||
start: Pos2,
|
||
},
|
||
/// A Zoom tool press with Animated Zoom ([`animated_zoom`]): dragged sideways it zooms about
|
||
/// `start`, held still (for `held` seconds so far) it zooms on, else it's a click.
|
||
ZoomScrub {
|
||
start: Pos2,
|
||
held: f32,
|
||
moved: bool,
|
||
},
|
||
/// A Rotate View tool drag, or a Space+Shift drag with any tool. With `snaps` (the Rotate
|
||
/// View tool), Shift rounds the angle to 15°.
|
||
RotateView {
|
||
start_angle: f64,
|
||
start_rot: f64,
|
||
snaps: bool,
|
||
},
|
||
/// A Selection tool move over a panel or toolbar: the art waits where it last moved on the
|
||
/// canvas, and the panels get it ([`widgets::PanelDrag::Art`]) if it is released there. Back
|
||
/// on the canvas the move goes on, as in Illustrator (#2826).
|
||
Art,
|
||
/// Fingers making a touch gesture ([`crate::touch`]): the first one's press does nothing.
|
||
Gesture,
|
||
/// A press in Presentation Mode: released as a click, it goes to the next artboard.
|
||
Present,
|
||
}
|
||
|
||
/// Whether the canvas is holding a pointer drag across frames.
|
||
pub(crate) fn dragging(ctx: &egui::Context) -> bool {
|
||
ctx.data(|d| d.get_temp::<Drag>(drag_id()).is_some())
|
||
}
|
||
|
||
/// The part of `outer` inside the scroll bars, and the part of that inside the rulers, which shows
|
||
/// the document. Normal Screen Mode and Full Screen Mode with Menu Bar keep the scroll bars along
|
||
/// the right and bottom edges; Full Screen and Presentation Mode have none.
|
||
fn view_rects(app: &VectorcraftApp, outer: egui::Rect, ppp: f32) -> (egui::Rect, egui::Rect) {
|
||
let full = if app.ui.screen_mode < 2 { egui::Rect::from_min_max(outer.min, outer.max - vec2(SCROLL_BAR, SCROLL_BAR)) } else { outer };
|
||
let rect = if app.ui.view.rulers && app.ui.screen_mode < 3 { egui::Rect::from_min_max(full.min + vec2(RULER, RULER), full.max) } else { full };
|
||
// Dock layout can differ by fractions of a device pixel between fresh windows.
|
||
// Use one pixel-aligned rectangle for drawing, inspection and pointer transforms.
|
||
(full, rect.round_to_pixels(ppp))
|
||
}
|
||
|
||
/// Paint the document canvas, complete or cached artwork, navigation and tool overlays.
|
||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||
let t = Tokens::get(ui.ctx());
|
||
let full = ui.available_rect_before_wrap();
|
||
// `hidden_chars` holds the marks Show Hidden Characters kept and the text nodes they were laid
|
||
// out from. Only the assignment right before the overlay call below puts them back. A frame
|
||
// that does not draw the overlay drops them, for example with the option off, with the edges
|
||
// hidden, on the Home screen or with no document open.
|
||
let hidden_chars = std::mem::take(&mut app.canvas.hidden_chars);
|
||
// A release ends the Image Trace panel's eye-button hold, also when the panel is no longer
|
||
// drawn, so a later press elsewhere does not show the source image.
|
||
if app.ui.trace_hold_source.is_some() && !ui.input(|i| i.pointer.any_down()) {
|
||
app.ui.trace_hold_source = None;
|
||
}
|
||
// The IME geometry this pass, if the Type tool gets one; `ime::end_of_pass` consumes it after
|
||
// the dialogs and panels draw.
|
||
app.ime_caret_rect = None;
|
||
// A document that opened, closed or became active since Home was chosen replaces it.
|
||
if app.ui.home.is_some_and(|k| k != crate::menus::home_key(app)) {
|
||
app.ui.home = None;
|
||
}
|
||
if crate::menus::home_showing(app) {
|
||
// No IME owner this pass (`ime_caret_rect` stays None): the end-of-pass observation marks
|
||
// the context absent and keeps any marked text as typed.
|
||
home(app, ui, full);
|
||
return;
|
||
}
|
||
if app.session.active().is_none() {
|
||
// No document and Show The Home Screen When No Documents Are Open off: an empty window.
|
||
ui.painter().rect_filled(full, 0.0, t.panel_darker);
|
||
return;
|
||
}
|
||
let outer = full;
|
||
let bars = app.ui.screen_mode < 2;
|
||
let (full, rect) = view_rects(app, outer, ui.ctx().pixels_per_point());
|
||
app.canvas_rect = Some(rect);
|
||
let fitted = app.view().is_some_and(|v| v.fitted);
|
||
// Presentation Mode shows the active artboard on the whole screen and nothing but the art:
|
||
// no selection, guides, grids or artboard edges (#4365).
|
||
let presenting = app.ui.screen_mode == 3;
|
||
if !fitted || (presenting && app.canvas.presented != Some(rect)) {
|
||
fit(app, "view.fitArtboard");
|
||
}
|
||
app.canvas.presented = presenting.then_some(rect);
|
||
// Before the canvas's own interaction, so the view the canvas draws is the one they moved to.
|
||
if bars {
|
||
scroll_bars(app, ui, rect, outer, &t);
|
||
}
|
||
let resp = ui.interact(rect, egui::Id::unique("canvas"), Sense::click_and_drag());
|
||
canvas_input(app, ui, &resp, rect);
|
||
let v = *app.view().unwrap_or(&View::default());
|
||
let xf = Xf::new(rect, &v);
|
||
// While the view pans, zooms or turns under the pointer, the art under it isn't hit-tested.
|
||
let wait = hover_wait(&mut app.canvas.view_moved, ViewKey { rect, zoom: v.zoom, center: v.center, rotation: v.rotation }, ui.input(|i| i.time));
|
||
if let Some(secs) = wait {
|
||
ui.ctx().request_repaint_after_secs(secs as f32);
|
||
}
|
||
let hover_still = wait.is_none();
|
||
let origin_preview = if app.ui.view.rulers && app.ui.screen_mode < 3 { ruler_input(app, ui, full, rect, &xf) } else { None };
|
||
crate::document_drag::drop_on_canvas(app, &resp, &xf);
|
||
panel_drop(app, ui, &resp, &xf);
|
||
// In Presentation Mode a right click or a long touch opens no context menu.
|
||
if app.ui.screen_mode < 3 {
|
||
context_menu(app, &resp, &xf);
|
||
}
|
||
let painter = ui.painter_at(rect);
|
||
let Some(st) = app.session.active() else { return };
|
||
let doc = st.doc.clone();
|
||
let mask_view = st.shown_mask();
|
||
|
||
paint_paper(app, st, ui, &painter, &xf, rect, &t);
|
||
// The grid behind the art (Guides & Grid › Grids In Back), else over it below.
|
||
let grid_look = (app.ui.view.grid && !presenting).then(|| LineLook::grid(&app.session.prefs));
|
||
let grids_in_back = app.session.prefs.grids_in_back;
|
||
if let Some(look) = grid_look.filter(|_| grids_in_back) {
|
||
grid(&painter, &xf, doc.grid.spacing, doc.grid.subdivisions, look);
|
||
}
|
||
|
||
// Artwork raster.
|
||
let ppp = ui.ctx().pixels_per_point();
|
||
// Match Renderer::render_as before recording the cache extent or building its view.
|
||
let (w, h) = (
|
||
((rect.width() * ppp).round().max(1.0) as u32).min(u32::from(u16::MAX)),
|
||
((rect.height() * ppp).round().max(1.0) as u32).min(u32::from(u16::MAX)),
|
||
);
|
||
// View › Pixel Preview: the art as it rasterizes, one pixel per point, while a document pixel
|
||
// is bigger than a screen pixel (below that the screen render already shows it).
|
||
let pixel = (app.ui.view.pixel_preview && v.zoom * ppp as f64 > 1.0).then(|| pixel_region(&xf)).flatten();
|
||
// File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview: off, images show
|
||
// their pixels as they rasterize, hard-edged.
|
||
let smooth_images = pixel.is_none() || app.session.prefs.anti_aliased_bitmaps;
|
||
let key = cache_key(app, ui, st, &t, &v, (w, h, ppp), (pixel, smooth_images));
|
||
// Placed documents' bitmaps are being made: draw again when they are ready.
|
||
if vectorcraft_render::placed_document::busy() {
|
||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(30));
|
||
}
|
||
poll_worker(app, ui, key.doc);
|
||
// With `waits_for_still` set (the web build, which has no render worker), a heavy document is
|
||
// not rendered while only the view changes: the frame shown is drawn reprojected below, and the
|
||
// first frame after the view has been still for STILL_SECS renders.
|
||
let wait = if app.canvas.waits_for_still {
|
||
let slow = app.canvas.last_ms > 8.0 && app.canvas.texture.is_some();
|
||
still_wait(&mut app.canvas.still, app.canvas.key.as_ref(), &key, slow, ui.input(|i| i.time))
|
||
} else {
|
||
None
|
||
};
|
||
if let Some(secs) = wait {
|
||
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(secs));
|
||
}
|
||
if wait.is_none() && (app.canvas.key.as_ref() != Some(&key) || app.canvas.texture.is_none()) {
|
||
let (w, h, view) = frame_view(pixel, (w, h), &v, ppp);
|
||
let opts = frame_options(app, &key, mask_view, smooth_images);
|
||
draw_frame(app, ui, Frame { key: &key, doc: &doc, size: (w, h), view, opts, pixel });
|
||
}
|
||
// The status bar reports each image that a renderer (this thread's or the worker's) could not
|
||
// decode, once per image in the process.
|
||
let undecoded = vectorcraft_render::newly_undecodable(doc.images.values());
|
||
if undecoded > 0 {
|
||
app.status(crate::i18n::msg_n(
|
||
undecoded as u64,
|
||
"{n} image couldn't be decoded and shows as a crossed box",
|
||
"{n} images couldn't be decoded and show as crossed boxes",
|
||
&[],
|
||
));
|
||
}
|
||
fill_uncovered(app, ui.ctx(), &painter, &key, &doc, rect, &xf);
|
||
reproject_frame(app, &painter, &xf, rect, (key.doc, v.rotation));
|
||
if let Some(look) = grid_look.filter(|_| !grids_in_back) {
|
||
grid(&painter, &xf, doc.grid.spacing, doc.grid.subdivisions, look);
|
||
}
|
||
// The pixel grid (Guides & Grid › Show Pixel Grid (Above 600% Zoom)): in Pixel Preview from
|
||
// 600% zoom, a line at every document pixel over the art, so the pixels it rasterizes to show.
|
||
let revolve_edit = app.ui.dialog.as_ref().is_some_and(crate::dialogs::revolve_gizmo::active);
|
||
let view_info = app.view_info();
|
||
if !presenting {
|
||
if app.ui.view.pixel_preview && v.zoom >= PIXEL_GRID_ZOOM && app.session.prefs.show_pixel_grid {
|
||
grid(&painter, &xf, 1.0, 1, LineLook { color: PIXEL_GRID, dots: false });
|
||
}
|
||
artboard_outlines(app, ui, &painter, &xf, &t);
|
||
if app.ui.view.guides {
|
||
guide_lines(app, &painter, &xf, rect, &t);
|
||
}
|
||
|
||
if !app.session.slices_hidden() {
|
||
slice_overlay(app, &painter, &xf);
|
||
}
|
||
if app.session.active().is_some_and(|d| d.print_tiling) || app.session.tool_id() == "printTiling" {
|
||
print_tiling_overlay(app, &painter, &xf);
|
||
}
|
||
original_path_overlay(app, &painter, &xf);
|
||
|
||
let overlays = selection_visuals(app, &painter, &xf, (hidden_chars, hover_still), revolve_edit, view_info);
|
||
draw_overlays(&painter, &xf, &overlays, &t, HandleLook::of(&app.session.prefs));
|
||
if !revolve_edit {
|
||
shaper_widget_overlay(app, &painter, &xf);
|
||
}
|
||
}
|
||
ime_geometry(app, ui.ctx(), &xf);
|
||
crate::place::paint_drop_highlight(app, ui.ctx(), &painter, rect);
|
||
// A Rotate View drag (the tool, or Space+Shift with any tool) shows the view's angle on a dial
|
||
// at the canvas center, the point the view turns about.
|
||
if app.ui.screen_mode < 3 && matches!(ui.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::RotateView { .. })) {
|
||
rotate_view_dial(&painter, rect.center(), v.rotation, &t);
|
||
}
|
||
|
||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||
ruler_bands(app, ui, &xf, (full, rect), origin_preview, &t);
|
||
}
|
||
// The task bar is not drawn while the Simplify or Smooth slider shows.
|
||
let strip = crate::dialogs::simplify_slider::showing(app) || crate::dialogs::smooth_slider::showing(app);
|
||
// It gives way to a drag, not to a click: hidden for the length of every press, it flashed as
|
||
// each click picked another object (#4681).
|
||
let dragging = app.session.tool_busy() && ui.input(|i| i.pointer.is_decidedly_dragging());
|
||
if app.ui.task_bar && !revolve_edit && !dragging && app.ui.screen_mode < 3 && !strip {
|
||
task_bar(app, ui, &xf);
|
||
}
|
||
if app.ui.screen_mode < 3 {
|
||
crate::free_transform::show(app, ui, xf.rect);
|
||
}
|
||
artboard_name_editor(app, ui, &xf);
|
||
if resp.hovered() {
|
||
canvas_cursor(app, ui, view_info, hover_still);
|
||
}
|
||
}
|
||
|
||
/// The pointer on the canvas: the tool's input, unless the context menu or the 3D gizmo has the press.
|
||
fn canvas_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect) {
|
||
// A press on the canvas while the context menu is open only closes it (a drag too, which egui
|
||
// alone would leave open); the tool doesn't get it.
|
||
if app.ui.dialog.as_ref().is_some_and(crate::dialogs::revolve_gizmo::active) {
|
||
// The artboard's 3D gizmo owns these gestures while its options are open.
|
||
// Do not let a ring drag also move or deselect the editable source path.
|
||
} else if resp.context_menu_opened() && resp.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||
egui::Popup::close_all(ui.ctx());
|
||
} else {
|
||
handle_input(app, ui, resp, rect);
|
||
// `hover_doc` is the document point under the pointer in the view `handle_input` left. The
|
||
// wheel, a pan, a Rotate View drag, the Zoom tool and a Presentation Mode click move the view
|
||
// there. A dialog, floating panel or menu over the canvas hides it (Object Highlighting
|
||
// showed through them: #4518), unless a drag that began on the canvas goes on there.
|
||
let moved = Xf::new(rect, app.view().unwrap_or(&View::default()));
|
||
let dragging = ui.data(|d| d.get_temp::<Drag>(drag_id()).is_some());
|
||
app.hover_doc =
|
||
ui.input(|i| i.pointer.hover_pos()).filter(|p| rect.contains(*p) && (dragging || drag::on_canvas(ui, resp, *p))).map(|p| moved.to_doc(p));
|
||
}
|
||
}
|
||
|
||
/// The pointer over the canvas: the view tools' cursors, else the tool's own.
|
||
fn canvas_cursor(app: &mut VectorcraftApp, ui: &Ui, view_info: vectorcraft_engine::ViewInfo, hover_still: bool) {
|
||
if opening::long_operation_busy(app) {
|
||
ui.ctx().set_cursor_icon(cursor_icon(Cursor::Busy));
|
||
return;
|
||
}
|
||
let m = ui.input(|i| i.modifiers);
|
||
let space = ui.input(|i| i.key_down(egui::Key::Space));
|
||
let drag = ui.data(|d| d.get_temp::<Drag>(drag_id()));
|
||
let panning = matches!(drag, Some(Drag::Pan { .. }));
|
||
// A zoom drag keeps its cursor when Space is pressed (with or without Shift) or Cmd is let go.
|
||
let zoom_drag = matches!(drag, Some(Drag::ZoomBox { .. } | Drag::ZoomScrub { .. }));
|
||
let zooming = zoom_drag || if space { m.command } else { app.session.tool_id() == "zoom" };
|
||
// A rotating drag keeps its cursor when Shift is let go.
|
||
let rotating = matches!(drag, Some(Drag::RotateView { .. }))
|
||
|| (!zoom_drag && if space { m.shift && !m.command } else { app.session.tool_id() == "rotateView" });
|
||
// The hand and the magnifier are drawn like the tool cursors below: Windows has no
|
||
// system cursor for them (winit shows the four-headed move cursor for Grab).
|
||
let view_cursor = if panning {
|
||
Some(Cursor::HandGrab)
|
||
} else if rotating {
|
||
Some(Cursor::Rotate)
|
||
} else if zooming {
|
||
Some(if m.alt { Cursor::ZoomOut } else { Cursor::ZoomIn })
|
||
} else if space || app.session.tool_id() == "hand" {
|
||
Some(Cursor::Hand)
|
||
} else {
|
||
None
|
||
};
|
||
// In Presentation Mode the pointer is the default one, with no tool cursor.
|
||
let tool_cursor = (app.ui.screen_mode < 3).then(|| view_cursor.or_else(|| hover_cursor(app, mods(m, space), view_info, hover_still)));
|
||
let cur = if let Some(c) = tool_cursor.flatten() {
|
||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::unique("tool-cursor")));
|
||
// The loaded place cursor carries the file's thumbnail.
|
||
if app.session.tool_id() == "place"
|
||
&& let Some(hp) = ui.input(|i| i.pointer.hover_pos())
|
||
{
|
||
crate::place::paint_cursor(app, ui.ctx(), &painter, hp);
|
||
}
|
||
// The glyph cursors are drawn here; General › Use Precise Cursors (the session turns the drawing tools' pointers
|
||
// into crosshairs, `Cursor::precise`) needs no handling. VECTORCRAFT_SYSTEM_CURSORS opts out of the glyphs.
|
||
let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none();
|
||
// User Interface › Scale Cursor Proportional to UI: the glyphs grow with UI Scaling;
|
||
// off, they keep their size.
|
||
let ui_scale = if app.session.prefs.scale_cursor_with_ui { 1.0 } else { ui.ctx().zoom_factor() };
|
||
match ui.input(|i| i.pointer.hover_pos()) {
|
||
// An OS cursor: the system moves it at once, where a painted one trails the pointer
|
||
// (#444). The system cursor stands in for cursors without a glyph. Over a window
|
||
// without the focus, macOS shows its own pointer whichever cursor the window sets,
|
||
// and the glyph is not painted there.
|
||
Some(_) if custom && crate::cursors::os_cursors(crate::cursors::OS_CURSORS, app.services.wayland) => {
|
||
let ppp = ui.ctx().pixels_per_point() / ui_scale;
|
||
ui.ctx().set_cursor_image(app.canvas.cursors.get(c, ppp));
|
||
cursor_icon(c)
|
||
}
|
||
Some(hp)
|
||
if custom
|
||
&& crate::cursors::paint_glyphs(crate::cursors::HIDES_ONLY_WHEN_KEY, ui.input(|i| i.focused))
|
||
&& crate::cursors::paint(&painter, c, hp, 1.0 / ui_scale) =>
|
||
{
|
||
// No cursor theme (Wayland): the glyph is painted, the system pointer stays (#2450).
|
||
if crate::cursors::hide_system_pointer(app.services.no_cursor_theme) { egui::CursorIcon::None } else { cursor_icon(c) }
|
||
}
|
||
_ => cursor_icon(c),
|
||
}
|
||
} else {
|
||
egui::CursorIcon::Default
|
||
};
|
||
ui.ctx().set_cursor_icon(cur);
|
||
}
|
||
|
||
/// The rulers, and the origin being dragged out of their corner.
|
||
fn ruler_bands(app: &VectorcraftApp, ui: &Ui, xf: &Xf, (full, rect): (egui::Rect, egui::Rect), origin_preview: Option<Point>, t: &Tokens) {
|
||
let painter = ui.painter_at(rect);
|
||
if let Some(origin) = origin_preview {
|
||
let at = xf.to_screen(origin);
|
||
painter.line_segment([pos2(at.x, rect.top()), pos2(at.x, rect.bottom())], Stroke::new(1.0, t.text_dim));
|
||
painter.line_segment([pos2(rect.left(), at.y), pos2(rect.right(), at.y)], Stroke::new(1.0, t.text_dim));
|
||
}
|
||
// View › Rulers › Show Video Rulers counts the active artboard in video pixels.
|
||
let video = app.ui.view.video_rulers.then(|| crate::dialogs::artboard_options::active_index(app));
|
||
let scale = match video.and_then(|i| app.session.active()?.doc.artboards.get(i)) {
|
||
Some(ab) => RulerScale::video(ab),
|
||
None => RulerScale::document(app.session.general_unit(), origin_preview.unwrap_or_else(|| app.ruler_origin())),
|
||
};
|
||
rulers(ui, full, xf, app.hover_doc, scale, t);
|
||
}
|
||
|
||
/// What the cached frame depends on.
|
||
fn cache_key(
|
||
app: &VectorcraftApp,
|
||
ui: &Ui,
|
||
st: &vectorcraft_engine::DocState,
|
||
t: &Tokens,
|
||
v: &View,
|
||
(w, h, ppp): (u32, u32, f32),
|
||
(pixel, smooth_images): (Option<[i64; 4]>, bool),
|
||
) -> CacheKey {
|
||
// Paths made into guides are drawn as the ruler guides are (Preferences › Guides & Grid).
|
||
let guide_look = LineLook::guides(&app.session.prefs, t);
|
||
CacheKey {
|
||
guide: ([guide_look.color.r(), guide_look.color.g(), guide_look.color.b()], guide_look.dots),
|
||
doc: st.uid as usize,
|
||
revision: st.revision,
|
||
interacting: st.interaction.is_some(),
|
||
zoom: v.zoom,
|
||
cx: v.center.x,
|
||
cy: v.center.y,
|
||
w,
|
||
h,
|
||
outline: app.outline_view(),
|
||
trim: app.ui.view.trim_view,
|
||
templates: app.ui.view.templates,
|
||
guides: app.ui.view.guides,
|
||
ppp,
|
||
hidden: vec![],
|
||
rot: v.rotation,
|
||
anti_alias: app.session.prefs.anti_aliased_artwork,
|
||
highlight_alternate_glyphs: app.session.prefs.highlight_alternate_glyphs,
|
||
highlight_substituted_fonts: app.session.prefs.highlight_substituted_fonts,
|
||
placed: vectorcraft_render::placed_document::generation(),
|
||
pixel,
|
||
smooth_images,
|
||
isolated: st.isolation.map(|id| id.0),
|
||
// Appearance of Black › On Screen: Display All Blacks as Rich Black.
|
||
rich_black: app.session.prefs.black_on_screen == "rich",
|
||
// A tool's drag on the canvas, during an interaction (not a dialog's preview).
|
||
dragging: app.session.in_interaction() && ui.data(|d| d.get_temp::<Drag>(drag_id())) == Some(Drag::Tool),
|
||
// The Image Trace panel's eye button, while it is held.
|
||
trace_source: app.ui.trace_hold_source.clone(),
|
||
}
|
||
}
|
||
|
||
/// Start the render worker, and upload the frame it finished.
|
||
fn poll_worker(app: &mut VectorcraftApp, ui: &Ui, key_doc: usize) {
|
||
if !app.canvas.worker_started {
|
||
app.canvas.worker_started = true;
|
||
if std::env::var_os("VECTORCRAFT_SYNC_RENDER").is_none() {
|
||
app.canvas.worker = crate::render_worker::Worker::spawn(ui.ctx().clone());
|
||
app.canvas.fill_worker = crate::render_worker::Worker::spawn(ui.ctx().clone());
|
||
}
|
||
}
|
||
// Upload finished background renders whenever they are not older than what is currently
|
||
// displayed on screen. This prevents canvas freezes during continuous edits (drags, arrow
|
||
// nudges, MCP command bursts) while ensuring obsolete out-of-order frames are dropped.
|
||
if let Some(done) = app.canvas.worker.as_mut().and_then(|w| w.poll())
|
||
&& done.key.doc == key_doc
|
||
{
|
||
let not_older = app.canvas.key.as_ref().filter(|k| k.doc == done.key.doc).is_none_or(|k| done.key.revision >= k.revision);
|
||
if not_older {
|
||
upload(app, ui.ctx(), done.img, done.key.pixel.is_some());
|
||
app.canvas.key = Some(done.key);
|
||
app.canvas.shown = Some((done.doc, done.opts));
|
||
app.perf.render_ms = done.ms;
|
||
app.canvas.last_ms = done.ms;
|
||
app.canvas.moved = done.moved > 0;
|
||
app.canvas.from_worker = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The size and view matrix of the frame to render: the pixel region in Pixel Preview, else the whole canvas.
|
||
fn frame_view(pixel: Option<[i64; 4]>, (w, h): (u32, u32), v: &View, ppp: f32) -> (u32, u32, Affine) {
|
||
match pixel {
|
||
Some([x0, y0, x1, y1]) => ((x1 - x0) as u32, (y1 - y0) as u32, Affine::translate((-x0 as f64, -y0 as f64))),
|
||
None => (
|
||
w,
|
||
h,
|
||
Affine::translate((w as f64 / 2.0, h as f64 / 2.0))
|
||
* Affine::rotate(v.rotation.to_radians())
|
||
* Affine::scale(v.zoom * ppp as f64)
|
||
* Affine::translate(-v.center.to_vec2()),
|
||
),
|
||
}
|
||
}
|
||
|
||
/// The options the canvas renders the document with.
|
||
fn frame_options(
|
||
app: &VectorcraftApp,
|
||
key: &CacheKey,
|
||
mask_view: Option<vectorcraft_doc::NodeId>,
|
||
smooth_images: bool,
|
||
) -> vectorcraft_render::RenderOptions {
|
||
let opts = vectorcraft_render::RenderOptions { outline: app.outline_view(), background: None, artboards: false, ..Default::default() };
|
||
vectorcraft_render::RenderOptions {
|
||
proof: vectorcraft_render::proof::active_proof(),
|
||
overprint_preview: vectorcraft_render::proof::overprint_preview_on(),
|
||
trim: app.ui.view.trim_view,
|
||
hide_templates: !app.ui.view.templates,
|
||
hide_guides: !app.ui.view.guides,
|
||
tile_edge: vectorcraft_color::Color::from_hex(&app.session.prefs.pattern_tile_edge_color).map_or(opts.tile_edge, |c| {
|
||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||
[r, g, b]
|
||
}),
|
||
guide: key.guide,
|
||
mask_view,
|
||
highlight_substitutions: true,
|
||
// Type › Highlight Alternate Glyphs (#394): pink behind OpenType substitutions.
|
||
highlight_alternate_glyphs: app.session.prefs.highlight_alternate_glyphs,
|
||
// Type › Highlight Substituted Fonts: pink behind text in a substitute font.
|
||
highlight_substituted_fonts: app.session.prefs.highlight_substituted_fonts,
|
||
// General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and
|
||
// pattern tiles stay smooth), as in Illustrator.
|
||
anti_alias: if app.session.prefs.anti_aliased_artwork { vectorcraft_render::AntiAlias::Art } else { vectorcraft_render::AntiAlias::None },
|
||
progressive_placed: true,
|
||
trace_views: true,
|
||
// Image Trace › the eye button held: the source image of the selected objects.
|
||
trace_source: key.trace_source.clone(),
|
||
smooth_images,
|
||
// Isolation mode: the art around the isolated group or layer is dimmed (#833).
|
||
isolated: key.isolated.map(vectorcraft_doc::NodeId),
|
||
// Images far smaller on screen than they are sample their mip levels (#1244).
|
||
image_mips: true,
|
||
// An image whose pixels can't be decoded shows as a crossed box, and the status message
|
||
// below reports it.
|
||
mark_undecoded_images: true,
|
||
// During a drag or a dialog's preview, objects that only moved draw their raster
|
||
// effects from the cached rasters of an earlier frame (#1257).
|
||
interacting: key.interacting,
|
||
// During a tool's drag, raster effects without a cached raster to draw render at a
|
||
// lower resolution (#2558); the frame after the drag renders them as a still frame does.
|
||
dragging: key.dragging,
|
||
rich_black: key.rich_black,
|
||
..opts
|
||
}
|
||
}
|
||
|
||
/// A frame to render: here, or on the worker.
|
||
struct Frame<'a> {
|
||
key: &'a CacheKey,
|
||
doc: &'a std::sync::Arc<vectorcraft_doc::Document>,
|
||
size: (u32, u32),
|
||
view: Affine,
|
||
opts: vectorcraft_render::RenderOptions,
|
||
pixel: Option<[i64; 4]>,
|
||
}
|
||
|
||
fn draw_frame(app: &mut VectorcraftApp, ui: &Ui, frame: Frame) {
|
||
let Frame { key, doc, size: (w, h), view, opts, pixel } = frame;
|
||
// to the worker. An interaction's frames stay on the worker once it drew the frame shown:
|
||
// its renderer keeps the rasters of the art's effects that make those frames fast, and this
|
||
// thread's renderer has none, so a frame fast on the worker would be slow here.
|
||
let same_doc = app.canvas.key.as_ref().is_some_and(|k| k.doc == key.doc);
|
||
let settling = app.canvas.moved && !key.interacting;
|
||
let staying = key.interacting && app.canvas.from_worker;
|
||
let heavy = (app.canvas.last_ms > 8.0 || settling || staying) && app.canvas.texture.is_some() && same_doc;
|
||
let changed = changed_pixels(app, key, doc, &opts, view, w, h);
|
||
match (&mut app.canvas.worker, heavy, changed) {
|
||
// Only the document changed since the frame shown, in a part of it: that part is
|
||
// rendered again, here, into the texture. `last_ms` stays the whole frame's.
|
||
(_, _, Some(region)) => {
|
||
let t0 = now_ms();
|
||
if let (Some([x0, y0, x1, y1]), Some(tex)) = (region, &mut app.canvas.texture) {
|
||
let part = app.canvas.renderer.render(doc, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view, &opts);
|
||
let color = color_image(&part);
|
||
let options = if pixel.is_some() { egui::TextureOptions::NEAREST } else { egui::TextureOptions::LINEAR };
|
||
tex.set_partial([x0 as usize, y0 as usize], color, options);
|
||
app.canvas.moved |= app.canvas.renderer.stats.moved > 0;
|
||
}
|
||
app.canvas.key = Some(key.clone());
|
||
app.canvas.shown = Some((doc.clone(), opts.clone()));
|
||
app.perf.render_ms = now_ms() - t0;
|
||
}
|
||
(Some(worker), true, None) => worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts }),
|
||
_ => {
|
||
let t0 = now_ms();
|
||
let img = app.canvas.renderer.render(doc, w, h, view, &opts);
|
||
upload(app, ui.ctx(), img, pixel.is_some());
|
||
app.canvas.key = Some(key.clone());
|
||
app.canvas.shown = Some((doc.clone(), opts.clone()));
|
||
app.perf.render_ms = now_ms() - t0;
|
||
app.canvas.last_ms = app.perf.render_ms;
|
||
app.canvas.moved = app.canvas.renderer.stats.moved > 0;
|
||
app.canvas.from_worker = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The pasteboard, the artboards' shadows and paper, and the outline mode's white artboards, of
|
||
/// document `st`.
|
||
fn paint_paper(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, ui: &Ui, painter: &egui::Painter, xf: &Xf, rect: egui::Rect, t: &Tokens) {
|
||
let doc = &st.doc;
|
||
// Pasteboard, artboard shadows and paper (Document Setup: the transparency grid's look, the
|
||
// simulated paper colour; a white Background Contents hides the grid).
|
||
// An artboard with a background color draws its paper in that color. With Simulate Colored Paper
|
||
// on, the canvas multiplies it with the paper color: each sRGB channel is artboard × paper / 255,
|
||
// rounded. The transparency grid, while it shows, covers it as it covers the others.
|
||
painter.rect_filled(rect, 0.0, t.pasteboard);
|
||
if !shows_artboards(app) {
|
||
// View › Hide Artboards: no edges and no pasteboard, the whole canvas paper (#4038).
|
||
let setup = &doc.setup;
|
||
painter.rect_filled(rect, 0.0, if setup.simulate_paper { crate::panels::c32(&setup.paper()) } else { Color32::WHITE });
|
||
} else if !app.outline_view() {
|
||
let setup = &doc.setup;
|
||
let grid = (st.transparency_grid && setup.background == vectorcraft_doc::Background::Transparent).then(|| checker_texture(ui.ctx(), setup));
|
||
let paper = if setup.simulate_paper { crate::panels::c32(&setup.paper()) } else { Color32::WHITE };
|
||
for ab in &doc.artboards {
|
||
let q = xf.quad(ab.rect);
|
||
// Hard 2 pt drop shadow, right and bottom (measured: #4d4d4d then #565656 on #606060).
|
||
let shift = |d: f32| q.iter().map(|p| *p + vec2(d, d)).collect::<Vec<_>>();
|
||
painter.add(Shape::convex_polygon(shift(2.0), Color32::from_black_alpha(26), Stroke::NONE));
|
||
painter.add(Shape::convex_polygon(shift(1.0), Color32::from_black_alpha(52), Stroke::NONE));
|
||
match &grid {
|
||
Some((tex, cell)) => checker(painter, &q, ab.rect, xf.zoom, tex.id(), *cell),
|
||
None => {
|
||
let fill = ab.color.map_or(paper, |c| crate::panels::c32(&c) * paper);
|
||
painter.add(Shape::convex_polygon(q, fill, Stroke::NONE));
|
||
}
|
||
}
|
||
}
|
||
} else if app.outline_view() {
|
||
for ab in &doc.artboards {
|
||
painter.add(Shape::convex_polygon(xf.quad(ab.rect), Color32::WHITE, Stroke::NONE));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The last rendered frame, placed through the current view while a new one is on its way.
|
||
fn reproject_frame(app: &VectorcraftApp, painter: &egui::Painter, xf: &Xf, rect: egui::Rect, (key_doc, rotation): (usize, f64)) {
|
||
if let (Some(tex), Some(k)) = (&app.canvas.texture, &app.canvas.key)
|
||
&& k.doc == key_doc
|
||
{
|
||
// The frame being shown covers the canvas rect it was rendered for. While a panel is
|
||
// dragged that rect is a different size, so reprojecting the frame through the *current*
|
||
// rect stretches the art to the new size until the worker's frame arrives, then it snaps
|
||
// back. Rebuild the rect it was rendered for from the key's own pixel size instead.
|
||
let old_rect = {
|
||
let ppp = if k.ppp > 0.0 { k.ppp } else { 1.0 };
|
||
egui::Rect::from_center_size(rect.center(), vec2(k.w as f32 / ppp, k.h as f32 / ppp))
|
||
};
|
||
match k.pixel {
|
||
// Document pixels: placed over the document rect they cover, turning with the view.
|
||
Some([x0, y0, x1, y1]) => {
|
||
textured_quad(painter, tex.id(), &xf.quad(Rect::new(x0 as f64, y0 as f64, x1 as f64, y1 as f64)), (1.0, 1.0));
|
||
}
|
||
None if (k.rot - rotation).abs() < 1e-9 => {
|
||
// Reproject the last frame if it was rendered for a different view.
|
||
let old = Xf { rect: old_rect, zoom: k.zoom, center: Point::new(k.cx, k.cy), rot: xf.rot };
|
||
let a = xf.to_screen(old.to_doc(old_rect.min));
|
||
let b = xf.to_screen(old.to_doc(old_rect.max));
|
||
painter.image(tex.id(), egui::Rect::from_min_max(a, b), egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||
}
|
||
// The view turned after the last frame was rendered, and the worker renders the frame at
|
||
// the new angle. Until it arrives, the last frame is drawn turned into this view: each
|
||
// corner goes where this view puts the document point that the corner showed.
|
||
None => {
|
||
let old = Xf { rect: old_rect, zoom: k.zoom, center: Point::new(k.cx, k.cy), rot: k.rot.to_radians() };
|
||
let q =
|
||
[old_rect.left_top(), old_rect.right_top(), old_rect.right_bottom(), old_rect.left_bottom()].map(|c| xf.to_screen(old.to_doc(c)));
|
||
textured_quad(painter, tex.id(), &q, (1.0, 1.0));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Do the artboards show? View › Hide Artboards hides them, except for the Artboard tool, which
|
||
/// edits them.
|
||
fn shows_artboards(app: &VectorcraftApp) -> bool {
|
||
app.ui.view.artboards || app.session.tool_id() == "artboard"
|
||
}
|
||
|
||
/// The artboards' edges, bleeds, marks and names.
|
||
fn artboard_outlines(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: &Xf, t: &Tokens) {
|
||
if !shows_artboards(app) {
|
||
return;
|
||
}
|
||
let Some(st) = app.session.active() else { return };
|
||
let doc = &st.doc;
|
||
// Artboard edges and names. The Artboard tool labels its selected artboards itself (following
|
||
// a move or resize): those aren't named twice.
|
||
let active_ab = crate::panels::artboards::selected(app, doc.artboards.len());
|
||
let tool_selected = artboards::tool_labelled(app);
|
||
let editing_name = ui.data(|d| d.get_temp::<ArtboardNameEdit>(name_edit_id()).map(|e| e.index));
|
||
let unlabelled = |i: usize| editing_name == Some(i) || u64::try_from(i).ok().is_some_and(|idx| tool_selected.contains(&idx));
|
||
artboard_edges(app, doc, (painter, xf, t), active_ab, unlabelled);
|
||
}
|
||
|
||
/// The edges, bleeds, marks and names of `doc`'s artboards, `active_ab` the active one; the
|
||
/// artboards `unlabelled` gives are not named.
|
||
fn artboard_edges(
|
||
app: &VectorcraftApp,
|
||
doc: &vectorcraft_doc::Document,
|
||
(painter, xf, t): (&egui::Painter, &Xf, &Tokens),
|
||
active_ab: usize,
|
||
unlabelled: impl Fn(usize) -> bool,
|
||
) {
|
||
let artboard_tool = app.session.tool_id() == "artboard";
|
||
for (i, ab) in doc.artboards.iter().enumerate() {
|
||
let c = if i == active_ab { Color32::from_gray(0) } else { Color32::from_gray(120) };
|
||
painter.add(Shape::closed_line(xf.quad(ab.rect), Stroke::new(if i == active_ab { 1.0 } else { 0.6 }, c)));
|
||
// The bleed (Document Setup) as a red outline around the artboard.
|
||
if doc.setup.has_bleed() {
|
||
painter.add(Shape::closed_line(xf.quad(doc.setup.bleed_rect(ab.rect)), Stroke::new(1.0, t.bleed)));
|
||
}
|
||
// Show Center Mark, Show Cross Hairs and Show Video Safe Areas (Artboard Options). Only the
|
||
// canvas draws them, as it does the edges: exports and print leave them out.
|
||
artboard_marks(painter, xf, ab, t.artboard_mark);
|
||
if (artboard_tool || doc.artboards.len() > 1) && !unlabelled(i) {
|
||
// At the artboard's own top-left corner, which a rotated view turns away from its
|
||
// bounding box's (#1095).
|
||
let corner = xf.to_screen(Point::new(ab.rect.x0, ab.rect.y0));
|
||
artboard_label(painter, corner, (ab.rect.width() * xf.zoom) as f32, &format!("{:02} - {}", i + 1, ab.name), t.text_dim);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The ruler guides.
|
||
fn guide_lines(app: &VectorcraftApp, painter: &egui::Painter, xf: &Xf, rect: egui::Rect, t: &Tokens) {
|
||
if let Some(st) = app.session.active() {
|
||
guides_of(app, st, (painter, xf, t), rect);
|
||
}
|
||
}
|
||
|
||
/// The ruler guides of document `st`, its selected guides in the selection colour.
|
||
fn guides_of(app: &VectorcraftApp, st: &vectorcraft_engine::DocState, (painter, xf, t): (&egui::Painter, &Xf, &Tokens), rect: egui::Rect) {
|
||
let doc = &st.doc;
|
||
let look = LineLook::guides(&app.session.prefs, t);
|
||
let picked = LineLook { color: t.selection, ..look };
|
||
let selected: &[usize] = &st.selection.guides;
|
||
let key = st.selection.key_guide;
|
||
let view = view_bounds(xf);
|
||
// Guides on a hidden layer hide with it.
|
||
for (i, g) in doc.guides.iter().enumerate().filter(|(_, g)| doc.guide_shown(g)) {
|
||
let at = |along: f64| xf.to_screen(if g.vertical { Point::new(g.pos, along) } else { Point::new(along, g.pos) });
|
||
// An artboard guide runs across its artboard, a canvas guide across the window (the part
|
||
// of its document line that the canvas shows, turned with the view).
|
||
let (a, b) = match doc.guide_span(g) {
|
||
Some((from, to)) => (at(from), at(to)),
|
||
None => {
|
||
let (from, to) = if g.vertical { (view.y0, view.y1) } else { (view.x0, view.x1) };
|
||
let Some(ends) = clip_segment(at(from), at(to), rect) else { continue };
|
||
ends
|
||
}
|
||
};
|
||
if selected.contains(&i) { picked } else { look }.guide(painter, a, b);
|
||
// The key guide (Align to Key Object, #2143): thicker, as a key object's outline is.
|
||
if key == Some(i) {
|
||
painter.line_segment([a, b], Stroke::new(KEY_OUTLINE, picked.color));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The selection's visuals and the tools' overlays, to be drawn.
|
||
fn selection_visuals(
|
||
app: &mut VectorcraftApp,
|
||
painter: &egui::Painter,
|
||
xf: &Xf,
|
||
(hidden_chars, hover_still): (HiddenChars, bool),
|
||
revolve_edit: bool,
|
||
view_info: vectorcraft_engine::ViewInfo,
|
||
) -> Vec<Overlay> {
|
||
// View → Hide Gradient Annotator hides the Gradient tool's annotator.
|
||
let mut overlays = if !revolve_edit && (app.session.tool_id() != "gradient" || app.ui.view.gradient_annotator) {
|
||
app.session.overlays(view_info)
|
||
} else {
|
||
vec![]
|
||
};
|
||
// Gap Options' Preview shows the gaps it finds.
|
||
overlays.extend(crate::dialogs::live_paint_gaps::overlays(app));
|
||
if app.ui.view.edges && !revolve_edit {
|
||
if hover_still {
|
||
hover_highlight(app, painter, xf);
|
||
}
|
||
selection_overlay(app, painter, xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. })));
|
||
draw_inside_overlay(app, painter, xf);
|
||
mirror_axis_overlay(app, painter, xf);
|
||
grid_repeat_overlay(app, painter, xf);
|
||
if app.ui.view.text_threads {
|
||
// Under the tools' overlays: a selected frame's own ports are drawn over its thread's.
|
||
let mut threads = thread_overlays(app, xf.zoom);
|
||
threads.append(&mut overlays);
|
||
overlays = threads;
|
||
}
|
||
if app.ui.view.live_paint_gaps {
|
||
overlays.extend(live_paint_gap_overlays(app));
|
||
}
|
||
if app.ui.view.hidden_chars {
|
||
app.canvas.hidden_chars = hidden_chars;
|
||
hidden_chars_overlay(app, painter, xf);
|
||
}
|
||
}
|
||
overlays
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|