mirror of
https://github.com/penpot/penpot.git
synced 2026-06-16 04:12:03 +00:00
🎉 Right click on guides to change color or remove
This commit is contained in:
parent
3916bfb334
commit
7c57bc56f1
@ -23,6 +23,8 @@
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.workspace.viewport.rulers :as rulers]
|
||||
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
@ -581,10 +583,17 @@
|
||||
:is-hover true
|
||||
:hover-frame frame}])]))
|
||||
|
||||
(defn- guide-by-serialized-index
|
||||
"Maps a WASM guide index back to the guide map entry. The index matches the
|
||||
serialization order used by `set-guides` / `write-guides`."
|
||||
[guides index]
|
||||
(when (>= index 0)
|
||||
(nth (vec (vals guides)) index nil)))
|
||||
|
||||
(mf/defc viewport-guides*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [zoom vbox hover-frame disabled-guides modifiers guides]}]
|
||||
(let [guides
|
||||
[{:keys [zoom vbox hover-frame disabled-guides modifiers guides wasm-guides?]}]
|
||||
(let [visible-guides
|
||||
(mf/with-memo [guides vbox]
|
||||
(->> (vals guides)
|
||||
(filter (partial guide-inside-vbox? zoom vbox))))
|
||||
@ -616,6 +625,23 @@
|
||||
(st/emit! (dw/show-guide-context-menu {:position position
|
||||
:guide guide})))))
|
||||
|
||||
;; When guides are WASM-rendered, right-click hit testing is delegated to
|
||||
;; the render engine instead of per-guide SVG areas.
|
||||
on-wasm-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps guides zoom disabled-guides)
|
||||
(fn [event]
|
||||
(when-not disabled-guides
|
||||
(let [position (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport position)
|
||||
index (when pt (wasm.api/find-guide-at pt zoom))
|
||||
guide (guide-by-serialized-index guides index)]
|
||||
(when guide
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(st/emit! (dw/show-guide-context-menu {:position position
|
||||
:guide guide})))))))
|
||||
|
||||
frame-modifiers
|
||||
(-> (group-by :id modifiers)
|
||||
(update-vals (comp :transform first)))]
|
||||
@ -623,6 +649,12 @@
|
||||
(mf/with-effect [hover-frame]
|
||||
(mf/set-ref-val! hover-frame-ref hover-frame))
|
||||
|
||||
(mf/with-effect [wasm-guides? disabled-guides on-wasm-context-menu]
|
||||
(when (and wasm-guides? (not disabled-guides))
|
||||
(when-let [viewport @uwvv/viewport-ref]
|
||||
(.addEventListener viewport "contextmenu" on-wasm-context-menu true)
|
||||
#(.removeEventListener viewport "contextmenu" on-wasm-context-menu true))))
|
||||
|
||||
[:g.guides {:pointer-events "none"}
|
||||
[:> new-guide-area* {:vbox vbox
|
||||
:zoom zoom
|
||||
@ -636,16 +668,17 @@
|
||||
:get-hover-frame get-hover-frame
|
||||
:disabled-guides disabled-guides}]
|
||||
|
||||
(for [{:keys [id frame-id] :as guide} guides]
|
||||
(when (or (nil? frame-id)
|
||||
(empty? focus)
|
||||
(contains? focus frame-id))
|
||||
[:> guide* {:key (dm/str "guide-" id)
|
||||
:guide guide
|
||||
:vbox vbox
|
||||
:zoom zoom
|
||||
:frame-transform (get frame-modifiers frame-id)
|
||||
:get-hover-frame get-hover-frame
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-context-menu on-guide-context-menu
|
||||
:disabled-guides disabled-guides}]))]))
|
||||
(when-not wasm-guides?
|
||||
(for [{:keys [id frame-id] :as guide} visible-guides]
|
||||
(when (or (nil? frame-id)
|
||||
(empty? focus)
|
||||
(contains? focus frame-id))
|
||||
[:> guide* {:key (dm/str "guide-" id)
|
||||
:guide guide
|
||||
:vbox vbox
|
||||
:zoom zoom
|
||||
:frame-transform (get frame-modifiers frame-id)
|
||||
:get-hover-frame get-hover-frame
|
||||
:on-guide-change on-guide-change
|
||||
:on-guide-context-menu on-guide-context-menu
|
||||
:disabled-guides disabled-guides}])))]))
|
||||
|
||||
@ -542,7 +542,7 @@
|
||||
;; change or their visibility toggles. When hidden we send an empty set.
|
||||
(mf/with-effect [@canvas-init? guides show-rulers? show-grids?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-guides (if (and show-rulers? show-grids?) guides {}))))
|
||||
(wasm.api/set-guides (if (and show-rulers? show-grids?) (or guides {}) {}))))
|
||||
|
||||
(hooks/setup-dom-events zoom disable-paste-ref in-viewport-ref read-only? drawing-tool path-drawing?)
|
||||
(hooks/setup-viewport-size vport viewport-ref)
|
||||
@ -837,8 +837,8 @@
|
||||
[:> guides/viewport-guides*
|
||||
{:zoom zoom
|
||||
:vbox vbox
|
||||
;; :guides guides
|
||||
:guides #{}
|
||||
:guides guides
|
||||
:wasm-guides? true
|
||||
:hover-frame guide-frame
|
||||
:disabled-guides disabled-guides?
|
||||
:modifiers wasm-modifiers}])
|
||||
|
||||
@ -2254,6 +2254,20 @@
|
||||
(h/call wasm/internal-module "_set_guides")
|
||||
(request-render "set-guides")))
|
||||
|
||||
;; Screen-space hit tolerance for ruler guides. Must match
|
||||
;; `guide-active-area` in `app.main.ui.workspace.viewport.guides`.
|
||||
(def ^:private guide-active-area 16)
|
||||
|
||||
(defn find-guide-at
|
||||
"Returns the serialized guide index at `position` (viewport coordinates),
|
||||
or -1 when no guide is within the hit tolerance."
|
||||
[position zoom]
|
||||
(h/call wasm/internal-module "_find_guide_at"
|
||||
(:x position)
|
||||
(:y position)
|
||||
zoom
|
||||
guide-active-area))
|
||||
|
||||
(defn get-grid-coords
|
||||
[position]
|
||||
(let [offset (h/call wasm/internal-module
|
||||
|
||||
@ -309,14 +309,14 @@
|
||||
"Total heap size (in bytes) needed to serialize `guides` (a map id -> guide),
|
||||
including the 4-byte header that holds the guide count."
|
||||
[guides]
|
||||
(+ 4 (* (count guides) guide-entry-size)))
|
||||
(+ 4 (* (count (or guides {})) guide-entry-size)))
|
||||
|
||||
(defn write-guides
|
||||
"Writes `guides` (a map id -> guide) into the heap views starting at the
|
||||
32-bit `offset`. Layout: count header (u32) followed by
|
||||
`kind | color | position` per guide."
|
||||
[guides heapu32 heapf32 offset]
|
||||
(let [guides (vec (vals guides))
|
||||
(let [guides (vec (vals (or guides {})))
|
||||
total (count guides)]
|
||||
(aset heapu32 offset total)
|
||||
(loop [i 0]
|
||||
|
||||
@ -865,8 +865,10 @@ impl RenderState {
|
||||
self.surfaces.clear_target(skia::Color::TRANSPARENT);
|
||||
self.surfaces.copy_backbuffer_to_target_replace();
|
||||
} else {
|
||||
self.surfaces.copy_backbuffer_to_target();
|
||||
self.surfaces
|
||||
.copy_backbuffer_to_target(self.background_color);
|
||||
}
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
}
|
||||
|
||||
@ -524,6 +524,7 @@ impl Surfaces {
|
||||
tile_viewbox: &TileViewbox,
|
||||
background: skia::Color,
|
||||
) {
|
||||
// self.backbuffer.canvas().clear(background);
|
||||
self.tiles.update(viewbox, tile_viewbox);
|
||||
let atlas_image = self.tile_atlas.image_snapshot();
|
||||
let canvas = self.backbuffer.canvas();
|
||||
@ -854,10 +855,17 @@ impl Surfaces {
|
||||
|
||||
/// Copy the current `Backbuffer` contents into `Target`.
|
||||
/// This is a GPU→GPU copy via Skia (no ReadPixels).
|
||||
pub fn copy_backbuffer_to_target(&mut self) {
|
||||
///
|
||||
/// `Target` is cleared to `background` first so UI overlay pixels (guides,
|
||||
/// grid) from the previous frame are fully erased. Without this, `SrcOver`
|
||||
/// compositing would keep stale overlay pixels wherever the backbuffer is
|
||||
/// transparent.
|
||||
pub fn copy_backbuffer_to_target(&mut self, background: skia::Color) {
|
||||
let sampling_options = self.sampling_options;
|
||||
let canvas = self.target.canvas();
|
||||
canvas.clear(background);
|
||||
self.backbuffer.draw(
|
||||
self.target.canvas(),
|
||||
canvas,
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
|
||||
@ -24,8 +24,10 @@ pub fn render_guide(canvas: &skia::Canvas, zoom: f32, area: Rect, guide: Guide)
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_color(Into::<skia::Color>::into(guide.color));
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(1.0 / zoom);
|
||||
// we disable antialias and increase the stroke thickness so the guides
|
||||
// do not appear faint or blurry.
|
||||
paint.set_anti_alias(false);
|
||||
paint.set_stroke_width(2.0 / zoom);
|
||||
|
||||
let (x1, y1, x2, y2) = match guide.kind {
|
||||
GuideKind::Vertical(x) => (x, area.top, x, area.bottom),
|
||||
|
||||
@ -15,6 +15,9 @@ impl GuidePool {
|
||||
}
|
||||
|
||||
pub fn set(&mut self, guides: Vec<Guide>) {
|
||||
self.horizontal.clear();
|
||||
self.vertical.clear();
|
||||
|
||||
for guide in guides {
|
||||
match guide.kind {
|
||||
GuideKind::Vertical(_) => self.vertical.push(guide),
|
||||
@ -56,6 +59,7 @@ impl GuidePool {
|
||||
return None;
|
||||
}
|
||||
|
||||
// FIXME: do binary search instead
|
||||
let idx = guides.partition_point(|guide| guide.position() < coord);
|
||||
let mut closest: Option<&Guide> = None;
|
||||
let mut closest_dist = world_tolerance;
|
||||
@ -96,8 +100,7 @@ impl UIState {
|
||||
self.guides.set(guides);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn find_guide_at(&self, x: f32, y: f32, zoom: f32, tolerance: f32) -> Option<&Guide> {
|
||||
pub fn find_guide_at(&self, x: f32, y: f32, zoom: f32, tolerance: f32) -> Option<&Guide> {
|
||||
self.guides.find_at(x, y, zoom, tolerance)
|
||||
}
|
||||
}
|
||||
@ -121,6 +124,31 @@ mod tests {
|
||||
pool
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_replaces_existing_guides() {
|
||||
let mut pool = pool_with(vec![vertical_guide(100.0, 0)]);
|
||||
pool.set(vec![vertical_guide(200.0, 0)]);
|
||||
|
||||
assert_eq!(pool.vertical.len(), 1);
|
||||
assert_eq!(pool.vertical[0].kind, GuideKind::Vertical(200.0));
|
||||
assert!(pool.horizontal.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_drops_removed_guides() {
|
||||
let mut pool = pool_with(vec![
|
||||
vertical_guide(100.0, 0),
|
||||
vertical_guide(200.0, 1),
|
||||
horizontal_guide(300.0, 2),
|
||||
]);
|
||||
pool.set(vec![vertical_guide(100.0, 0), horizontal_guide(300.0, 1)]);
|
||||
|
||||
assert_eq!(pool.vertical.len(), 1);
|
||||
assert_eq!(pool.horizontal.len(), 1);
|
||||
assert_eq!(pool.vertical[0].kind, GuideKind::Vertical(100.0));
|
||||
assert_eq!(pool.horizontal[0].kind, GuideKind::Horizontal(300.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_at_returns_none_when_no_guides() {
|
||||
let pool = GuidePool::new();
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use crate::mem;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
globals::get_ui_state,
|
||||
globals::{get_render_state, get_ui_state},
|
||||
ui::{Guide, GuideKind},
|
||||
};
|
||||
use crate::with_state;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
const RAW_GUIDE_SIZE: usize = std::mem::size_of::<RawGuide>();
|
||||
@ -93,5 +94,21 @@ pub extern "C" fn set_guides() -> Result<()> {
|
||||
get_ui_state().set_guides(guides);
|
||||
|
||||
mem::free_bytes()?;
|
||||
|
||||
// Guides are drawn on the UI overlay composited onto `Target`. Refresh the
|
||||
// presented frame immediately so removed guides do not linger as stale pixels.
|
||||
with_state!(state, {
|
||||
get_render_state().present_frame(&state.shapes);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_error]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn find_guide_at(x: f32, y: f32, zoom: f32, tolerance: f32) -> Result<i32> {
|
||||
Ok(get_ui_state()
|
||||
.find_guide_at(x, y, zoom, tolerance)
|
||||
.map(|guide| guide.index as i32)
|
||||
.unwrap_or(-1))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user