Manage rotation and color of selection size badge (#10393)

*  Rotate and move size badge when shape is rotated

* 🐛 Fix wrong text color in selection size badge

*  Add tests

* ♻️ Extract common constants
This commit is contained in:
Luis de Dios 2026-07-02 14:59:33 +02:00 committed by GitHub
parent f1426cb3bb
commit d795a1f110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 239 additions and 85 deletions

View File

@ -104,6 +104,60 @@ test("Selection size badge appears on selection and hides on deselect", async ({
await expect(badge).toHaveCount(0);
});
test("Selection size badge uses component color for component selection", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("components/get-file-13267.json");
await workspacePage.goToWorkspace({
fileId: "e9c84e12-dd29-80fc-8007-86d559dced7f",
pageId: "e9c84e12-dd29-80fc-8007-86d559dced80",
});
await workspacePage.clickLeafLayer("A Component");
const badge = page.locator(".selection-size-badge");
await expect(badge).toBeVisible();
await expect(badge.locator("rect")).toHaveCSS("fill", "rgb(187, 151, 216)");
await expect(badge.locator("text")).toHaveCSS("fill", "rgb(255, 255, 255)");
});
test("Selection size badge shows unrotated dimensions for rotated single selection", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
const badgeText = page.locator(".selection-size-badge text");
await expect(badgeText).toHaveText("126 x 134");
const rotationInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Rotation",
});
await rotationInput.fill("45");
await rotationInput.press("Enter");
await expect(rotationInput).toHaveValue("45");
await expect(badgeText).toHaveText("126 x 134");
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();

View File

@ -308,3 +308,24 @@
normal progress becomes tagged as slow if no event received in the
specified amount of time"
1000)
;; ------------------------------------------------
;; Typography
;; ------------------------------------------------
(def ^:const font-size 11)
;; ------------------------------------------------
;; Colors (CSS custom properties)
;; ------------------------------------------------
(def ^:const select-color "var(--color-accent-tertiary)")
(def ^:const distance-color "var(--color-accent-quaternary)")
(def ^:const distance-text-color "var(--app-white)")
;; ------------------------------------------------
;; Selection rectangle & guides
;; ------------------------------------------------
(def ^:const selection-rect-width 1)

View File

@ -1,5 +1,6 @@
(ns app.main.ui.flex-controls.common
(:require
[app.main.constants :as mconst]
[app.main.ui.formats :as fmt]
[rumext.v2 :as mf]))
@ -7,10 +8,8 @@
;; CONSTANTS
;; ------------------------------------------------
(def font-size 11)
(def distance-color "var(--color-accent-quaternary)")
(def distance-text-color "var(--app-white)")
(def warning-color "var(--status-color-warning-500)")
(def flex-display-pill-width 40)
(def flex-display-pill-height 20)
(def flex-display-pill-border-radius 4)
@ -30,6 +29,6 @@
:y (+ y (/ height 2))
:text-anchor "middle"
:dominant-baseline "central"
:style {:fill distance-text-color
:style {:fill mconst/distance-text-color
:font-size font-size}}
(fmt/format-number (or value 0))]])

View File

@ -15,6 +15,7 @@
[app.common.geom.shapes.points :as gpo]
[app.common.types.modifiers :as ctm]
[app.common.types.shape.layout :as ctl]
[app.main.constants :as mconst]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
@ -109,7 +110,7 @@
:on-pointer-down on-move-selected
:on-context-menu on-context-menu
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
:opacity (if is-selected 0.5 0.25)}}]
(let [handle-width
@ -134,7 +135,7 @@
:on-context-menu on-context-menu
:class (when (or is-hover is-selected)
(if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90)))
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
:opacity (if is-selected 0 1)}}])]))
(mf/defc gap-rects*
@ -341,9 +342,9 @@
[:& fcc/flex-display-pill
{:height pill-height
:width pill-width
:font-size (/ fcc/font-size zoom)
:font-size (/ mconst/font-size zoom)
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
:color fcc/distance-color
:color mconst/distance-color
:x (:x @mouse-pos)
:y (- (:y @mouse-pos) pill-width)
:value @hover-value}])]))

View File

@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.types.modifiers :as ctm]
[app.main.constants :as mconst]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
@ -224,7 +225,7 @@
[:& fcc/flex-display-pill
{:height pill-height
:width pill-width
:font-size (/ fcc/font-size zoom)
:font-size (/ mconst/font-size zoom)
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
:color fcc/warning-color
:x (:x @mouse-pos)

View File

@ -9,6 +9,7 @@
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.types.modifiers :as ctm]
[app.main.constants :as mconst]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
@ -115,7 +116,7 @@
:on-pointer-move on-pointer-move
:on-pointer-down on-move-selected
:on-context-menu on-context-menu
:style {:fill (if (or is-hover is-selected) fcc/distance-color "none")
:style {:fill (if (or is-hover is-selected) mconst/distance-color "none")
:opacity (if is-selected 0.5 0.25)}}]
(let [handle-width
@ -145,7 +146,7 @@
(cur/get-dynamic "resize-ew" 90)))
:style
{:fill (if (or is-hover is-selected) fcc/distance-color "none")
{:fill (if (or is-hover is-selected) mconst/distance-color "none")
:opacity (if is-selected 0 1)}}])]))
(mf/defc padding-rects*
@ -267,9 +268,9 @@
[:& fcc/flex-display-pill
{:height pill-height
:width pill-width
:font-size (/ fcc/font-size zoom)
:font-size (/ mconst/font-size zoom)
:border-radius (/ fcc/flex-display-pill-border-radius zoom)
:color fcc/distance-color
:color mconst/distance-color
:x (:x @mouse-pos)
:y (- (:y @mouse-pos) pill-width)
:value @hover-value}])]))

View File

@ -8,18 +8,10 @@
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.main.constants :as mconst]
[app.main.ui.measurements :refer [size-display* measurement*]]
[rumext.v2 :as mf]))
;; ------------------------------------------------
;; CONSTANTS
;; ------------------------------------------------
(def select-color "var(--color-accent-tertiary)")
(def selection-rect-width 1)
(def select-guide-width 1)
(def select-guide-dasharray 5)
(defn resolve-shapes
[objects ids]
(let [resolve-shape (d/getf objects)]
@ -41,14 +33,14 @@
(mf/defc selection-rect [{:keys [selrect zoom]}]
(let [{:keys [x y width height]} selrect
selection-rect-width (/ selection-rect-width zoom)]
selection-rect-width (/ mconst/selection-rect-width zoom)]
[:g.selection-rect
[:rect {:x x
:y y
:width width
:height height
:style {:fill "none"
:stroke select-color
:stroke mconst/select-color
:stroke-width selection-rect-width}}]]))
(mf/defc selection-feedback

View File

@ -13,7 +13,9 @@
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.constants :as mconst]
[app.main.ui.formats :as fmt]
[rumext.v2 :as mf]))
@ -21,36 +23,30 @@
;; CONSTANTS
;; ------------------------------------------------
(def font-size 11)
(def selection-rect-width 1)
(def ^:private ^:const size-display-color "var(--app-white)")
(def ^:private ^:const size-display-opacity 0.7)
(def ^:private ^:const size-display-text-color "var(--app-black)")
(def ^:private ^:const size-display-width-min 50)
(def ^:private ^:const size-display-width-max 75)
(def ^:private ^:const size-display-height 16)
(def select-color "var(--color-accent-tertiary)")
(def select-guide-width 1)
(def select-guide-dasharray 5)
(def hover-color "var(--color-accent-quaternary)")
(def size-display-color "var(--app-white)")
(def size-display-opacity 0.7)
(def size-display-text-color "var(--app-black)")
(def size-display-width-min 50)
(def size-display-width-max 75)
(def size-display-height 16)
(def distance-color "var(--color-accent-quaternary)")
(def distance-text-color "var(--app-white)")
(def distance-border-radius 2)
(def distance-pill-width 50)
(def distance-pill-height 16)
(def distance-line-stroke 1)
(def ^:private ^:const distance-border-radius 2)
(def ^:private ^:const distance-pill-width 50)
(def ^:private ^:const distance-pill-height 16)
(def ^:private ^:const distance-line-stroke 1)
(def ^:private ^:const selection-badge-bg-color "var(--color-accent-tertiary)")
(def ^:private ^:const selection-badge-bg-color-component "var(--color-accent-secondary)")
(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)
(def ^:private ^:const select-guide-width 1)
(def ^:private ^:const select-guide-dasharray 5)
(def ^:private ^:const hover-color "var(--color-accent-quaternary)")
;; ------------------------------------------------
;; HELPERS
@ -128,13 +124,13 @@
:height rect-height
:text-anchor "middle"
:style {:fill size-display-text-color
:font-size (/ font-size zoom)}}
:font-size (/ mconst/font-size zoom)}}
size-label]]))
(mf/defc distance-display-pill* [{:keys [x y zoom distance bounds]}]
(let [distance-pill-width (/ distance-pill-width zoom)
distance-pill-height (/ distance-pill-height zoom)
font-size (/ font-size zoom)
font-size (/ mconst/font-size zoom)
text-padding (/ 3 zoom)
distance-border-radius (/ distance-border-radius zoom)
@ -162,7 +158,7 @@
:ry distance-border-radius
:width distance-pill-width
:height distance-pill-height
:style {:fill distance-color}}]
:style {:fill mconst/distance-color}}]
[:text {:x (+ text-x offset-x)
:y (+ text-y offset-y)
@ -171,13 +167,13 @@
:text-anchor "middle"
:width distance-pill-width
:height distance-pill-height
:style {:fill distance-text-color
:style {:fill mconst/distance-text-color
:font-size font-size}}
(fmt/format-pixels distance)]]))
(mf/defc selection-rect* [{:keys [selrect zoom]}]
(let [{:keys [x y width height]} selrect
selection-rect-width (/ selection-rect-width zoom)]
selection-rect-width (/ mconst/selection-rect-width zoom)]
[:g.selection-rect
[:rect {:x x
:y y
@ -187,34 +183,124 @@
:stroke hover-color
:stroke-width selection-rect-width}}]]))
(defn- get-edge-for-badge
"For each rotation range, select the 'most horizontal' edge at the bottom,
as seen from the user's perspective."
[rotation]
(let [rot (mod rotation 360)]
(cond
(or (< rot 45) (>= rot 315)) :bottom
(< rot 135) :right
(< rot 225) :top
:else :left)))
(defn- get-edge-points
"Get the points that define the selected side of a rectangle"
[points edge]
(let [[p0 p1 p2 p3] points]
(case edge
:bottom [p2 p3]
:right [p1 p2]
:top [p0 p1]
:left [p3 p0])))
(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]]))
[{:keys [zoom shapes]}]
(let [badge-height (/ selection-badge-height zoom)
badge-padding-x (/ selection-badge-padding-x zoom)
badge-gap (/ selection-badge-vertical-gap zoom)
badge-radius (/ selection-badge-border-radius zoom)
badge-char-width (/ selection-badge-char-width zoom)
single-shape (and (= (count shapes) 1) (first shapes))
component-color? (if single-shape
(ctk/instance-head? single-shape)
(every? ctk/instance-head? shapes))
badge-bg-color (if component-color?
selection-badge-bg-color-component
selection-badge-bg-color)
rotation (when single-shape (dm/get-prop single-shape :rotation))
has-rotation? (and rotation (not (mth/almost-zero? rotation)))
;; Always compute the selrect from :points via shapes->rect.
;; This gives the correct bounding box for all shape types,
;; including component instances and shapes with transforms.
selrect (gsh/shapes->rect shapes)
;; For single shapes we show the original dimensions,
;; for multiple shapes show the bounding box
shape-width (if single-shape
(dm/get-prop single-shape :width)
(:width selrect))
shape-height (if single-shape
(dm/get-prop single-shape :height)
(:height selrect))
text (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height))
text-width (* (count text) badge-char-width)
badge-width (+ text-width (* 2 badge-padding-x))]
(if has-rotation?
(let [edge (get-edge-for-badge rotation)
points (dm/get-prop single-shape :points)
[ep1 ep2] (get-edge-points points edge)
mid-point (gpt/lerp ep1 ep2 0.5)
normal (gpt/normal-right (gpt/subtract ep2 ep1))
rot-offset (case edge
:bottom 0
:right 270
:top 180
:left 90)
badge-rot (+ rotation rot-offset)
offset (+ badge-gap (/ badge-height 2))
badge-x (- (/ badge-width 2))
badge-y (- (/ badge-height 2))
badge-cx (+ (:x mid-point) (* (:x normal) offset))
badge-cy (+ (:y mid-point) (* (:y normal) offset))]
[:g.selection-size-badge {:pointer-events "none"
:transform (dm/str "translate(" badge-cx "," badge-cy ") rotate(" badge-rot ")")}
[:rect {:x badge-x
:y badge-y
:width badge-width
:height badge-height
:rx badge-radius
:ry badge-radius
:style {:fill badge-bg-color}}]
[:text {:class (stl/css :badge-text)
:x 0
:y 0
:text-anchor "middle"
:dominant-baseline "middle"}
text]])
(let [badge-x (- (/ badge-width 2))
badge-y (- (/ badge-height 2))
badge-cx (+ (:x selrect) (/ (:width selrect) 2))
badge-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2))]
[:g.selection-size-badge {:pointer-events "none"
:transform (dm/str "translate(" badge-cx "," badge-cy ")")}
[:rect {:x badge-x
:y badge-y
:width badge-width
:height badge-height
:rx badge-radius
:ry badge-radius
:style {:fill badge-bg-color}}]
[:text {:class (stl/css :badge-text)
:x 0
:y 0
:text-anchor "middle"
:dominant-baseline "middle"}
text]]))))
(mf/defc distance-display* [{:keys [from to zoom bounds]}]
(let [fixed-x (if (gsh/fully-contained? from to)
@ -245,7 +331,7 @@
:y1 y1
:x2 x2
:y2 y2
:style {:stroke distance-color
:style {:stroke mconst/distance-color
:stroke-width distance-line-stroke}}]
[:> distance-display-pill*
@ -263,7 +349,7 @@
:y1 y1
:x2 x2
:y2 y2
:style {:stroke select-color
:style {:stroke mconst/select-color
:stroke-width (/ select-guide-width zoom)
:stroke-dasharray (/ select-guide-dasharray zoom)}}])])

View File

@ -4,10 +4,9 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_utils.scss" as *;
.badge-text {
fill: var(--app-black);
font-size: calc(deprecated.$fs-12 / var(--zoom));
font-family: "worksans", "vazirmatn", sans-serif;
fill: var(--color-static-white);
font-size: calc(px2rem(12) / var(--zoom));
}

View File

@ -508,7 +508,7 @@
(not edition)
(not mode-inspect?))
[:> msr/selection-size-badge*
{:selrect (gsh/shapes->rect selected-shapes)
{:shapes selected-shapes
:zoom zoom}])
(when show-measures?

View File

@ -779,7 +779,7 @@
(not mode-inspect?)
(not page-transition?))
[:> msr/selection-size-badge*
{:selrect (gsh/shapes->rect selected-shapes)
{:shapes selected-shapes
:zoom zoom}])
(when show-measures?