Implement the Properties panel with a transform section for layers (#527)

* initial layout system with tool options

* cargo fmt

* cargo fmt again

* document bar defined on the backend

* cargo fmt

* removed RC<RefCell>

* cargo fmt

* - fix increment behavior
- removed hashmap from layout message handler
- removed no op message from layoutMessage

* cargo fmt

* only send documentBar when zoom or rotation is updated

* ctrl-0 changes zoom properly

* unfinished layer hook in

* fix layerData name

* layer panel options bar

* basic x/y movment

* working transform section

* changed messages from tuples to structs

* hook up text input

* - fixed number input to be more clear
- fixed actions for properties message handler

* Add styling

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mfish33 2022-02-12 08:22:57 -08:00 committed by Keavon Chambers
parent d084775d81
commit 91e4201cb1
19 changed files with 659 additions and 45 deletions

View file

@ -6,12 +6,12 @@
<Separator :type="'Section'" />
<WidgetLayout :layout="toolOptionsLayout" />
<WidgetLayout :layout="toolOptionsLayout" class="tool-options" />
</LayoutRow>
<LayoutRow class="spacer"></LayoutRow>
<WidgetLayout :layout="documentBarLayout" class="right side" />
<WidgetLayout :layout="documentBarLayout" class="right side document-bar" />
</LayoutRow>
<LayoutRow class="shelf-and-viewport">
<LayoutCol class="shelf">

View file

@ -1,15 +1,64 @@
<template>
<LayoutCol class="properties-panel"></LayoutCol>
<LayoutCol class="properties">
<LayoutRow class="options-bar">
<WidgetLayout :layout="propertiesOptionsLayout"></WidgetLayout>
</LayoutRow>
<LayoutRow class="sections" :scrollableY="true">
<WidgetLayout :layout="propertiesSectionsLayout"></WidgetLayout>
</LayoutRow>
</LayoutCol>
</template>
<style lang="scss"></style>
<style lang="scss">
.properties {
height: 100%;
.widget-layout {
flex: 1 1 100%;
margin: 0 4px;
}
.options-bar {
height: 32px;
flex: 0 0 auto;
}
.sections {
flex: 1 1 100%;
}
}
</style>
<script lang="ts">
import { defineComponent } from "vue";
import { defaultWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/dispatcher/js-messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
export default defineComponent({
components: { LayoutCol },
inject: ["editor", "dialog"],
data() {
return {
propertiesOptionsLayout: defaultWidgetLayout(),
propertiesSectionsLayout: defaultWidgetLayout(),
};
},
mounted() {
this.editor.dispatcher.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
this.propertiesOptionsLayout = updatePropertyPanelOptionsLayout;
});
this.editor.dispatcher.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
this.propertiesSectionsLayout = updatePropertyPanelSectionsLayout;
});
},
components: {
WidgetLayout,
LayoutRow,
LayoutCol,
},
});
</script>

View file

@ -12,7 +12,6 @@
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
</style>

View file

@ -1,4 +1,5 @@
<template>
<div>{{ widgetData.name }}</div>
<div class="widget-row">
<template v-for="(component, index) in widgetData.widgets" :key="index">
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
@ -13,18 +14,20 @@
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
/>
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="component.props">{{ component.props.value }}</TextLabel>
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
</template>
</div>
</template>
<style lang="scss">
.widget-row {
height: 100%;
height: 32px;
flex: 0 0 auto;
display: flex;
align-items: center;
@ -42,6 +45,8 @@ import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
export default defineComponent({
@ -63,6 +68,8 @@ export default defineComponent({
IconButton,
OptionalInput,
RadioInput,
TextLabel,
IconLabel,
},
});
</script>

View file

@ -1,26 +1,78 @@
<!-- TODO: Implement collapsable sections with properties system -->
<template>
<div class="widget-section">
<template v-for="(layoutRow, index) in widgetData.layout" :key="index">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget"></component>
</template>
</div>
<LayoutCol class="widget-section">
<LayoutRow class="header" @click.stop="() => (expanded = !expanded)">
<div class="expand-arrow" :class="{ expanded }"></div>
<Separator :type="'Related'" />
<TextLabel :bold="true">{{ widgetData.name }}</TextLabel>
</LayoutRow>
<LayoutCol class="body" v-if="expanded">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget" v-for="(layoutRow, index) in widgetData.layout" :key="index"></component>
</LayoutCol>
</LayoutCol>
</template>
<style lang="scss">
.widget-section {
height: 100%;
flex: 0 0 auto;
display: flex;
align-items: center;
.header {
flex: 0 0 24px;
background: var(--color-4-dimgray);
align-items: center;
padding: 0 8px;
margin: 0 -4px;
.expand-arrow {
width: 6px;
height: 100%;
padding: 0;
position: relative;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
&::after {
content: "";
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 3px 0 3px 6px;
border-color: transparent transparent transparent var(--color-e-nearwhite);
}
&.expanded::after {
border-width: 6px 3px 0 3px;
border-color: var(--color-e-nearwhite) transparent transparent transparent;
}
}
.text-label {
height: 18px;
display: inline-block;
}
}
.body {
margin: 0 4px;
.text-label {
flex: 0 0 30%;
text-align: right;
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetSection as WidgetSectionFromJsMessages } from "@/dispatcher/js-messages";
import { isWidgetRow, isWidgetSection, LayoutRow as LayoutSystemRow, WidgetSection as WidgetSectionFromJsMessages } from "@/dispatcher/js-messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
import Separator from "@/components/widgets/separators/Separator.vue";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
const WidgetSection = defineComponent({
@ -34,21 +86,27 @@ const WidgetSection = defineComponent({
return {
isWidgetRow,
isWidgetSection,
expanded: true,
};
},
methods: {
updateLayout(widgetId: BigInt, value: unknown) {
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
},
layoutRowType(layoutRow: LayoutRow): unknown {
layoutRowType(layoutRow: LayoutSystemRow): unknown {
if (isWidgetRow(layoutRow)) return WidgetRow;
if (isWidgetSection(layoutRow)) return WidgetSection;
throw new Error("Layout row type does not exist");
},
},
components: { WidgetRow },
components: {
LayoutCol,
LayoutRow,
TextLabel,
Separator,
WidgetRow,
},
});
export default WidgetSection;
</script>

View file

@ -185,8 +185,9 @@ export default defineComponent({
// Find the amount of digits on the left side of the decimal
// 10.25 == 2
// 1.23 == 1
// 0.23 == 0 (reason for the slightly more complicated code)
const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value);
// 0.23 == 0 (Reason for the slightly more complicated code)
const absValueInt = Math.floor(Math.abs(value));
const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length;
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
const displayValue = Math.round(value * roundingPower) / roundingPower;

View file

@ -12,7 +12,13 @@
></FieldInput>
</template>
<style lang="scss"></style>
<style lang="scss">
.text-input {
input {
text-align: left;
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "vue";
@ -20,7 +26,7 @@ import { defineComponent, PropType } from "vue";
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
export default defineComponent({
emits: ["update:value"],
emits: ["update:value", "commitText"],
props: {
value: { type: String as PropType<string>, required: true },
label: { type: String as PropType<string>, required: false },
@ -54,7 +60,13 @@ export default defineComponent({
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
onTextChanged() {
// The `inputElement.blur()` call in `onCancelTextChange()` causes itself to be run again, so this if statement skips a second run
if (this.editing) this.onCancelTextChange();
if (!this.editing) return;
this.onCancelTextChange();
// TODO: Find a less hacky way to do this
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
this.$emit("commitText", inputElement.value);
},
onCancelTextChange() {
this.editing = false;

View file

@ -412,7 +412,7 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
return Boolean((layoutRow as WidgetSection).layout);
}
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput";
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel";
export interface Widget {
kind: WidgetKind;
@ -428,7 +428,21 @@ export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
layout!: LayoutRow[];
}
export class UpdateDocumentBarLayout extends JsMessage {
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
}
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
}
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
@ -457,7 +471,7 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
if (rowOrSection.Section) {
return {
name: rowOrSection.Section.name,
layout: createWidgetLayout(rowOrSection.Section),
layout: createWidgetLayout(rowOrSection.Section.layout),
};
}
@ -508,6 +522,8 @@ export const messageConstructors: Record<string, MessageMaker> = {
UpdateInputHints,
UpdateMouseCursor,
UpdateOpenDocumentsList,
UpdatePropertyPanelOptionsLayout,
UpdatePropertyPanelSectionsLayout,
UpdateToolOptionsLayout,
UpdateWorkingColors,
} as const;