diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 24c31a0061..51f407c1e8 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -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(); diff --git a/frontend/src/app/main/constants.cljs b/frontend/src/app/main/constants.cljs index f838005d48..42e6a02e32 100644 --- a/frontend/src/app/main/constants.cljs +++ b/frontend/src/app/main/constants.cljs @@ -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) diff --git a/frontend/src/app/main/ui/flex_controls/common.cljs b/frontend/src/app/main/ui/flex_controls/common.cljs index f9f5e15ab1..1aa28a5289 100644 --- a/frontend/src/app/main/ui/flex_controls/common.cljs +++ b/frontend/src/app/main/ui/flex_controls/common.cljs @@ -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))]]) diff --git a/frontend/src/app/main/ui/flex_controls/gap.cljs b/frontend/src/app/main/ui/flex_controls/gap.cljs index 710b89882f..38926c9887 100644 --- a/frontend/src/app/main/ui/flex_controls/gap.cljs +++ b/frontend/src/app/main/ui/flex_controls/gap.cljs @@ -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}])])) diff --git a/frontend/src/app/main/ui/flex_controls/margin.cljs b/frontend/src/app/main/ui/flex_controls/margin.cljs index 20f486fdeb..d8ad044355 100644 --- a/frontend/src/app/main/ui/flex_controls/margin.cljs +++ b/frontend/src/app/main/ui/flex_controls/margin.cljs @@ -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) diff --git a/frontend/src/app/main/ui/flex_controls/padding.cljs b/frontend/src/app/main/ui/flex_controls/padding.cljs index b751c565d3..c10f83cd31 100644 --- a/frontend/src/app/main/ui/flex_controls/padding.cljs +++ b/frontend/src/app/main/ui/flex_controls/padding.cljs @@ -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}])])) diff --git a/frontend/src/app/main/ui/inspect/selection_feedback.cljs b/frontend/src/app/main/ui/inspect/selection_feedback.cljs index 5e3f57f169..3679be0283 100644 --- a/frontend/src/app/main/ui/inspect/selection_feedback.cljs +++ b/frontend/src/app/main/ui/inspect/selection_feedback.cljs @@ -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 diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index aca38aa5d8..b42a1a18af 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -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)}}])]) diff --git a/frontend/src/app/main/ui/measurements.scss b/frontend/src/app/main/ui/measurements.scss index 660f9e745f..4babfe43dc 100644 --- a/frontend/src/app/main/ui/measurements.scss +++ b/frontend/src/app/main/ui/measurements.scss @@ -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)); } diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 6eccc2fa41..939e63100e 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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? diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index da9303f483..ca67ec8f11 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -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?