diff --git a/CHANGES.md b/CHANGES.md index edacca0551..57d8fc11f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ ### :sparkles: New features & Enhancements +- Show a read-only W × H size badge below the bounding box of the current selection so dimensions are visible directly on the canvas (initial implementation: always-on badge, single visual variant, no live-resize integration; remaining spec items follow in subsequent PRs) [Github #9205](https://github.com/penpot/penpot/issues/9205) + ### :bug: Bugs fixed - Harden Nginx responses with standard security headers and hide upstream `X-Powered-By` headers diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index e6169e92c8..6006d888ab 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -78,6 +78,32 @@ test("User draws a rect", async ({ page }) => { await expect(workspacePage.canvas).toHaveScreenshot(); }); +test("Selection size badge appears on selection and hides on deselect", async ({ + page, +}) => { + const workspacePage = new WasmWorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-not-empty.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + const badge = page.locator(".selection-size-badge"); + + await expect(badge).toHaveCount(0); + + await workspacePage.clickLeafLayer("Rectangle"); + await expect(badge).toBeVisible(); + + await workspacePage.page.keyboard.press("Escape"); + await expect(badge).toHaveCount(0); +}); + test("User makes a group", async ({ page }) => { const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index 0e9d823996..923ce0788c 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -5,6 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.measurements + (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -43,6 +44,13 @@ (def distance-pill-height 16) (def distance-line-stroke 1) +(def ^:private ^:const selection-badge-bg-color "var(--color-accent-tertiary)") +(def ^:private ^:const selection-badge-height 16) +(def ^:private ^:const selection-badge-padding-x 6) +(def ^:private ^:const selection-badge-vertical-gap 8) +(def ^:private ^:const selection-badge-border-radius 2) +(def ^:private ^:const selection-badge-char-width 6.5) + ;; ------------------------------------------------ ;; HELPERS @@ -179,6 +187,35 @@ :stroke hover-color :stroke-width selection-rect-width}}]])) +(mf/defc selection-size-badge* + [{:keys [selrect zoom]}] + (let [{:keys [x y width height]} selrect + size-label (dm/str (fmt/format-number width) " x " (fmt/format-number height)) + badge-height (/ selection-badge-height zoom) + padding-x (/ selection-badge-padding-x zoom) + gap (/ selection-badge-vertical-gap zoom) + radius (/ selection-badge-border-radius zoom) + text-width (* (count size-label) (/ selection-badge-char-width zoom)) + badge-width (+ text-width (* 2 padding-x)) + center-x (+ x (/ width 2)) + badge-x (- center-x (/ badge-width 2)) + badge-y (+ y height gap) + text-y (+ badge-y (/ badge-height 2))] + [:g.selection-size-badge {:pointer-events "none"} + [:rect {:x badge-x + :y badge-y + :width badge-width + :height badge-height + :rx radius + :ry radius + :style {:fill selection-badge-bg-color}}] + [:text {:class (stl/css :badge-text) + :x center-x + :y text-y + :text-anchor "middle" + :dominant-baseline "middle"} + size-label]])) + (mf/defc distance-display* [{:keys [from to zoom bounds]}] (let [fixed-x (if (gsh/fully-contained? from to) (+ (:x to) (/ (:width to) 2)) @@ -244,6 +281,7 @@ :bounds bounds :zoom zoom}] [:> size-display* {:selrect selected-selrect :zoom zoom}] + [:> selection-size-badge* {:selrect selected-selrect :zoom zoom}] (if (or (not hover-shape) (not hover-selected-shape?)) (when (and frame (not= uuid/zero (:id frame))) diff --git a/frontend/src/app/main/ui/measurements.scss b/frontend/src/app/main/ui/measurements.scss new file mode 100644 index 0000000000..660f9e745f --- /dev/null +++ b/frontend/src/app/main/ui/measurements.scss @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "refactor/common-refactor.scss" as deprecated; + +.badge-text { + fill: var(--app-black); + font-size: calc(deprecated.$fs-12 / var(--zoom)); + font-family: "worksans", "vazirmatn", sans-serif; +} diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a77bcdfeb7..194575231c 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -500,6 +500,14 @@ :zoom zoom :modifiers modifiers}]) + (when (and (seq selected-shapes) + (not transform) + (not text-editing?) + (not edition)) + [:> msr/selection-size-badge* + {:selrect (gsh/shapes->rect selected-shapes) + :zoom zoom}]) + (when show-measures? [:> msr/measurement* {:bounds vbox diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 000e8bd010..86efd4d067 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -653,6 +653,15 @@ {:shape (get base-objects edition) :zoom zoom}]) + (when (and (seq selected-shapes) + (not transform) + (not text-editing?) + (not edition) + (not page-transition?)) + [:> msr/selection-size-badge* + {:selrect (gsh/shapes->rect selected-shapes) + :zoom zoom}]) + (when show-measures? [:> msr/measurement* {:bounds vbox