From f4adfe56be8ebdb8ead8b5a7fc1dfddd53d80e83 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 28 Jul 2025 11:57:26 +0200 Subject: [PATCH] :sparkles: Add defaults for artboard drawing --- CHANGES.md | 1 + .../src/app/main/data/workspace/drawing.cljs | 33 ++++++ .../main/data/workspace/drawing/common.cljs | 34 +++--- .../app/main/data/workspace/transforms.cljs | 9 +- .../main/ui/workspace/sidebar/options.cljs | 8 +- .../ui/workspace/sidebar/options/drawing.cljs | 23 ++++ .../sidebar/options/drawing/frame.cljs | 99 ++++++++++++++++ .../sidebar/options/drawing/frame.scss | 110 ++++++++++++++++++ .../sidebar/options/menus/measures.cljs | 1 + 9 files changed, 294 insertions(+), 24 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/drawing.cljs create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss diff --git a/CHANGES.md b/CHANGES.md index 6c9eb8700e..8cbc3c59f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ ### :heart: Community contributions (Thank you!) ### :sparkles: New features & Enhancements +- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047) - Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/issue/1780) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index a3b77f82ed..8b2c211613 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -8,6 +8,7 @@ "Drawing interactions." (:require [app.common.data.macros :as dm] + [app.common.math :as mth] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.box :as box] @@ -100,5 +101,37 @@ :curve (curve/handle-drawing) (box/handle-drawing type)))))) +(defn change-orientation + [orientation] + + (assert + (contains? #{:horizontal :vertical} orientation) + "expected valid orientation") + + (ptk/reify ::change-orientation + ptk/UpdateEvent + (update [_ state] + (let [{:keys [width height]} + (get state :workspace-drawing) + + width' + (if (= orientation :vertical) + (mth/min width height) + (mth/max width height)) + + height' + (if (= orientation :vertical) + (mth/max width height) + (mth/min width height))] + + (update state :workspace-drawing assoc :width width' :height height'))))) + +(defn set-default-size + [width height] + (ptk/reify ::change-preset + ptk/UpdateEvent + (update [_ state] + (update state :workspace-drawing assoc :width width :height height)))) + diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 7097fa30c8..77dd595852 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -6,10 +6,8 @@ (ns app.main.data.workspace.drawing.common (:require - [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] - [app.common.math :as mth] [app.common.types.modifiers :as ctm] [app.common.types.path :as path] [app.common.types.shape :as cts] @@ -25,27 +23,35 @@ (ptk/reify ::clear-drawing ptk/UpdateEvent (update [_ state] - (update state :workspace-drawing dissoc :tool :object)))) + (dissoc state :workspace-drawing)))) (defn handle-finish-drawing [] (ptk/reify ::handle-finish-drawing ptk/WatchEvent (watch [_ state _] - (let [tool (dm/get-in state [:workspace-drawing :tool]) - shape (dm/get-in state [:workspace-drawing :object]) - objects (dsh/lookup-page-objects state) - page-id (:current-page-id state)] + (let [drawing-state + (get state :workspace-drawing) + + shape + (get drawing-state :object) + + tool + (get drawing-state :tool) + + objects + (dsh/lookup-page-objects state) + + page-id + (:current-page-id state)] (rx/concat (when (:initialized? shape) (let [click-draw? (:click-draw? shape) text? (cfh/text-shape? shape) - vbox (dm/get-in state [:workspace-local :vbox]) - min-side (mth/min 100 - (mth/floor (dm/get-prop vbox :width)) - (mth/floor (dm/get-prop vbox :height))) + width (get drawing-state :width 100) + height (get drawing-state :height 100) shape (cond-> shape @@ -53,14 +59,14 @@ (assoc :grow-type :fixed) (and ^boolean click-draw? (not ^boolean text?)) - (-> (assoc :width min-side) - (assoc :height min-side) + (-> (assoc :width width) + (assoc :height height) ;; NOTE: we need to recalculate the selrect and ;; points, so we assign `nil` to it (assoc :selrect nil) (assoc :points nil) (cts/setup-shape) - (gsh/transform-shape (ctm/move-modifiers (- (/ min-side 2)) (- (/ min-side 2))))) + (gsh/transform-shape (ctm/move-modifiers (- (/ width 2)) (- (/ height 2))))) (and click-draw? text?) (-> (assoc :height 17 :width 4 :grow-type :auto-width) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index caaa75149f..299e65d273 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -551,9 +551,7 @@ (defn start-move-selected "Enter mouse move mode, until mouse button is released." - ([] - (start-move-selected nil false)) - + ([] (start-move-selected nil false)) ([id shift?] (ptk/reify ::start-move-selected ptk/WatchEvent @@ -972,8 +970,9 @@ (defn move-selected "Move shapes a fixed increment in one direction, from a keyboard action." [direction shift?] - (dm/assert! (contains? valid-directions direction)) - (dm/assert! (boolean? shift?)) + + (assert (contains? valid-directions direction)) + (assert (boolean? shift?)) (ptk/reify ::move-selected ptk/WatchEvent diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 1ebc0fd53a..5c16891da1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -19,6 +19,7 @@ [app.main.ui.context :as ctx] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] [app.main.ui.inspect.right-sidebar :as hrs] + [app.main.ui.workspace.sidebar.options.drawing :as drawing] [app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]] [app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options]] [app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu]] @@ -109,11 +110,8 @@ [:& specialized-panel {:panel sp-panel}] (d/not-empty? drawing) - [:> shape-options* - {:shape (:object drawing) - :page-id page-id - :file-id file-id - :libraries libraries}] + [:> drawing/drawing-options* + {:drawing-state drawing}] (= 0 (count selected)) [:> page/options*] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing.cljs new file mode 100644 index 0000000000..35ad6e16fa --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing.cljs @@ -0,0 +1,23 @@ + +;; 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 + +(ns app.main.ui.workspace.sidebar.options.drawing + (:require + [app.main.ui.workspace.sidebar.options.drawing.frame :as frame] + [rumext.v2 :as mf])) + +(mf/defc drawing-options* + {::mf/wrap [#(mf/throttle % 60)] + ::mf/private true} + [{:keys [drawing-state] :as props}] + (case (:tool drawing-state) + :frame + [:> frame/options* {:drawing-state drawing-state}] + + nil)) + + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs new file mode 100644 index 0000000000..654ae7654e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.cljs @@ -0,0 +1,99 @@ +;; 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 + +(ns app.main.ui.workspace.sidebar.options.drawing.frame + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.constants :refer [size-presets]] + [app.main.data.workspace.drawing :as dwd] + [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc options* + [{:keys [drawing-state]}] + + (let [show* (mf/use-state false) + show? (deref show*) + + on-open + (mf/use-fn + (fn [] (reset! show* true))) + + on-close + (mf/use-fn + (fn [] (reset! show* false))) + + on-preset-selected + (mf/use-fn + (fn [event] + (let [width (-> (dom/get-current-target event) + (dom/get-data "width") + (d/read-string)) + height (-> (dom/get-current-target event) + (dom/get-data "height") + (d/read-string))] + (st/emit! (dwd/set-default-size width height))))) + + orientation + (when (:width drawing-state) + (if (> (:width drawing-state) (:height drawing-state)) + :horizontal + :vertical)) + + on-orientation-change + (mf/use-fn + (fn [orientation] + (let [orientation (keyword orientation)] + (st/emit! (dwd/change-orientation orientation)))))] + + [:div {:class (stl/css :presets)} + [:div {:class (stl/css-case :presets-wrapper true + :opened show?) + :on-click on-open} + [:span {:class (stl/css :select-name)} (tr "workspace.options.size-presets")] + [:span {:class (stl/css :collapsed-icon)} i/arrow] + [:& dropdown {:show show? + :on-close on-close} + [:ul {:class (stl/css :custom-select-dropdown)} + (for [preset size-presets] + (if-not (:width preset) + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :disabled true)} + [:span {:class (stl/css :preset-name)} (:name preset)]] + + (let [preset-match (and (= (:width preset) (:width drawing-state)) + (= (:height preset) (:height drawing-state)))] + [:li {:key (:name preset) + :class (stl/css-case :dropdown-element true + :match preset-match) + :data-width (str (:width preset)) + :data-height (str (:height preset)) + :on-click on-preset-selected} + [:div {:class (stl/css :name-wrapper)} + [:span {:class (stl/css :preset-name)} (:name preset)] + [:span {:class (stl/css :preset-size)} (:width preset) " x " (:height preset)]] + (when preset-match + [:span {:class (stl/css :check-icon)} i/tick])])))]]] + + [:& radio-buttons {:selected (or (d/name orientation) "") + :on-change on-orientation-change + :name "frame-orientation" + :wide true + :class (stl/css :radio-buttons)} + [:& radio-button {:icon i/size-vertical + :value "vertical" + :id "size-vertical"}] + [:& radio-button {:icon i/size-horizontal + :value "horizontal" + :id "size-horizontal"}]]])) + diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss new file mode 100644 index 0000000000..fdedbc7d83 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/drawing/frame.scss @@ -0,0 +1,110 @@ +// 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 + +@import "../../../../ds/_sizes.scss"; +@import "refactor/common-refactor.scss"; + +.presets { + display: grid; + grid-template-columns: repeat(8, var(--sp-xxxl)); + gap: var(--sp-xs); + grid-column: 1 / -1; +} + +.presets-wrapper { + @extend .asset-element; + position: relative; + grid-column: span 5; + display: flex; + height: $s-32; + padding: $s-8; + border-radius: $br-8; + + .collapsed-icon { + @include flexCenter; + cursor: pointer; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + transform: rotate(90deg); + } + } + + &:hover { + .collapsed-icon svg { + stroke: var(--input-foreground-color-active); + } + } +} + +.radio-buttons { + grid-column: span 2; +} + +.select-name { + @include bodySmallTypography; + display: flex; + justify-content: flex-start; + align-items: center; + flex-grow: 1; + cursor: pointer; +} + +.custom-select-dropdown { + @extend .dropdown-wrapper; + margin-top: $s-2; + max-height: 70vh; + width: $s-252; + .dropdown-element { + @extend .dropdown-element-base; + .name-wrapper { + display: flex; + gap: $s-8; + flex-grow: 1; + .preset-name { + color: var(--menu-foreground-color-rest); + } + .preset-size { + color: var(--menu-foreground-color-rest); + } + } + + .check-icon { + @include flexCenter; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } + } + + &.disabled { + pointer-events: none; + cursor: default; + .preset-name { + color: var(--menu-foreground-color); + } + } + + &.match { + .name-wrapper .preset-name { + color: var(--menu-foreground-color-hover); + } + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + + &:hover { + background-color: var(--menu-background-color-hover); + .name-wrapper .preset-name { + color: var(--menu-foreground-color-hover); + } + .check-icon svg { + stroke: var(--menu-foreground-color-hover); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index 67c9528b63..bfb460c624 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -338,6 +338,7 @@ :aria-label (tr "workspace.options.fit-content") :on-pointer-down handle-fit-content :icon "fit-content"}]]) + (when (options :size) [:div {:class (stl/css :size)} [:div {:class (stl/css-case :width true