Skip to content

Commit

Permalink
Add UI for anchor/position dropdowns (#2940)
Browse files Browse the repository at this point in the history
* Add UI for anchor/position dropdowns

* Fixed MenuIconRowGroup
  • Loading branch information
RunDevelopment authored Jun 4, 2024
1 parent 09fb933 commit ee0b03f
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 145 deletions.
47 changes: 46 additions & 1 deletion backend/src/nodes/properties/inputs/generic_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class DropDownOption(TypedDict):
condition: NotRequired[ConditionJson | None]


DropDownStyle = Literal["dropdown", "checkbox", "tabs", "icons"]
DropDownStyle = Literal["dropdown", "checkbox", "tabs", "icons", "anchor"]
"""
This specified the preferred style in which the frontend may display the dropdown.
Expand All @@ -53,6 +53,7 @@ class DropDownOption(TypedDict):
The first option will be interpreted as the yes/true option while the second option will be interpreted as the no/false option.
- `tabs`: The options are displayed as tab list. The label of the input itself will *not* be displayed.
- `icons`: The options are displayed as a list of icons. This is only available if all options have icons. Labels are still required for all options.
- `anchor`: The options are displayed as a 3x3 grid where the user is allowed to select one of 9 anchor positions. This only works for dropdowns with 9 options.
"""


Expand Down Expand Up @@ -613,3 +614,47 @@ def RowOrderDropdown() -> DropDownInput:
label="Order",
default=OrderEnum.ROW_MAJOR,
)


class Anchor(Enum):
TOP_LEFT = "top_left"
TOP = "top_centered"
TOP_RIGHT = "top_right"
LEFT = "centered_left"
CENTER = "centered"
RIGHT = "centered_right"
BOTTOM_LEFT = "bottom_left"
BOTTOM = "bottom_centered"
BOTTOM_RIGHT = "bottom_right"


def AnchorInput(label: str = "Anchor", icon: str = "BsFillImageFill") -> DropDownInput:
return EnumInput(
Anchor,
label=label,
label_style="inline",
option_labels={
Anchor.TOP_LEFT: "Top Left",
Anchor.TOP: "Top",
Anchor.TOP_RIGHT: "Top Right",
Anchor.LEFT: "Left",
Anchor.CENTER: "Center",
Anchor.RIGHT: "Right",
Anchor.BOTTOM_LEFT: "Bottom Left",
Anchor.BOTTOM: "Bottom",
Anchor.BOTTOM_RIGHT: "Bottom Right",
},
icons={
Anchor.TOP_LEFT: icon,
Anchor.TOP: icon,
Anchor.TOP_RIGHT: icon,
Anchor.LEFT: icon,
Anchor.CENTER: icon,
Anchor.RIGHT: icon,
Anchor.BOTTOM_LEFT: icon,
Anchor.BOTTOM: icon,
Anchor.BOTTOM_RIGHT: icon,
},
preferred_style="anchor",
default=Anchor.CENTER,
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from nodes.impl.color.color import Color
from nodes.impl.image_utils import normalize, to_uint8
from nodes.properties.inputs import (
Anchor,
AnchorInput,
BoolInput,
ColorInput,
EnumInput,
Expand All @@ -31,64 +33,22 @@
]


class TextAsImageAlignment(Enum):
class TextAlignment(Enum):
LEFT = "left"
CENTER = "center"
RIGHT = "right"


class TextAsImagePosition(Enum):
TOP_LEFT = "top_left"
TOP_CENTERED = "top_centered"
TOP_RIGHT = "top_right"
CENTERED_LEFT = "centered_left"
CENTERED = "centered"
CENTERED_RIGHT = "centered_right"
BOTTOM_LEFT = "bottom_left"
BOTTOM_CENTERED = "bottom_centered"
BOTTOM_RIGHT = "bottom_right"


TEXT_AS_IMAGE_POSITION_LABELS = {
TextAsImagePosition.TOP_LEFT: "Top left",
TextAsImagePosition.TOP_CENTERED: "Top centered",
TextAsImagePosition.TOP_RIGHT: "Top right",
TextAsImagePosition.CENTERED_LEFT: "Centered left",
TextAsImagePosition.CENTERED: "Centered",
TextAsImagePosition.CENTERED_RIGHT: "Centered right",
TextAsImagePosition.BOTTOM_LEFT: "Bottom left",
TextAsImagePosition.BOTTOM_CENTERED: "Bottom centered",
TextAsImagePosition.BOTTOM_RIGHT: "Bottom right",
}

TEXT_AS_IMAGE_X_Y_REF_FACTORS = {
TextAsImagePosition.TOP_LEFT: {"x": np.array([0, 0.5]), "y": np.array([0, 0.5])},
TextAsImagePosition.TOP_CENTERED: {
"x": np.array([0.5, 0]),
"y": np.array([0, 0.5]),
},
TextAsImagePosition.TOP_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0, 0.5])},
TextAsImagePosition.CENTERED_LEFT: {
"x": np.array([0, 0.5]),
"y": np.array([0.5, 0]),
},
TextAsImagePosition.CENTERED: {"x": np.array([0.5, 0]), "y": np.array([0.5, 0])},
TextAsImagePosition.CENTERED_RIGHT: {
"x": np.array([1, -0.5]),
"y": np.array([0.5, 0]),
},
TextAsImagePosition.BOTTOM_LEFT: {
"x": np.array([0, 0.5]),
"y": np.array([1, -0.5]),
},
TextAsImagePosition.BOTTOM_CENTERED: {
"x": np.array([0.5, 0]),
"y": np.array([1, -0.5]),
},
TextAsImagePosition.BOTTOM_RIGHT: {
"x": np.array([1, -0.5]),
"y": np.array([1, -0.5]),
},
X_Y_REF_FACTORS = {
Anchor.TOP_LEFT: {"x": np.array([0, 0.5]), "y": np.array([0, 0.5])},
Anchor.TOP: {"x": np.array([0.5, 0]), "y": np.array([0, 0.5])},
Anchor.TOP_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0, 0.5])},
Anchor.LEFT: {"x": np.array([0, 0.5]), "y": np.array([0.5, 0])},
Anchor.CENTER: {"x": np.array([0.5, 0]), "y": np.array([0.5, 0])},
Anchor.RIGHT: {"x": np.array([1, -0.5]), "y": np.array([0.5, 0])},
Anchor.BOTTOM_LEFT: {"x": np.array([0, 0.5]), "y": np.array([1, -0.5])},
Anchor.BOTTOM: {"x": np.array([0.5, 0]), "y": np.array([1, -0.5])},
Anchor.BOTTOM_RIGHT: {"x": np.array([1, -0.5]), "y": np.array([1, -0.5])},
}


Expand All @@ -105,26 +65,21 @@ class TextAsImagePosition(Enum):
BoolInput("Italic", default=False, icon="FaItalic").with_id(2),
),
EnumInput(
TextAsImageAlignment,
TextAlignment,
label="Alignment",
preferred_style="icons",
icons={
TextAsImageAlignment.LEFT: "FaAlignLeft",
TextAsImageAlignment.CENTER: "FaAlignCenter",
TextAsImageAlignment.RIGHT: "FaAlignRight",
TextAlignment.LEFT: "FaAlignLeft",
TextAlignment.CENTER: "FaAlignCenter",
TextAlignment.RIGHT: "FaAlignRight",
},
default=TextAsImageAlignment.CENTER,
default=TextAlignment.CENTER,
).with_id(4),
),
ColorInput(channels=[3], default=Color.bgr((0, 0, 0))).with_id(3),
NumberInput("Width", min=1, max=None, default=500).with_id(5),
NumberInput("Height", min=1, max=None, default=100).with_id(6),
EnumInput(
TextAsImagePosition,
label="Position",
option_labels=TEXT_AS_IMAGE_POSITION_LABELS,
default=TextAsImagePosition.CENTERED,
).with_id(7),
NumberInput("Width", min=1, max=None, default=500, unit="px").with_id(5),
NumberInput("Height", min=1, max=None, default=100, unit="px").with_id(6),
AnchorInput(label="Position", icon="MdTextFields").with_id(7),
],
outputs=[
ImageOutput(
Expand All @@ -143,11 +98,11 @@ def text_as_image_node(
text: str,
bold: bool,
italic: bool,
alignment: TextAsImageAlignment,
alignment: TextAlignment,
color: Color,
width: int,
height: int,
position: TextAsImagePosition,
position: Anchor,
) -> np.ndarray:
path = TEXT_AS_IMAGE_FONT_PATH[int(bold)][int(italic)]
font_path = os.path.join(
Expand Down Expand Up @@ -178,11 +133,11 @@ def text_as_image_node(
drawing = ImageDraw.Draw(pil_image)

x_ref = round(
np.sum(np.array([width, w_text]) * TEXT_AS_IMAGE_X_Y_REF_FACTORS[position]["x"]) # type: ignore
np.sum(np.array([width, w_text]) * X_Y_REF_FACTORS[position]["x"]) # type: ignore
)
y_ref = round(
np.sum(
np.array([height, h_text]) * TEXT_AS_IMAGE_X_Y_REF_FACTORS[position]["y"] # type: ignore
np.array([height, h_text]) * X_Y_REF_FACTORS[position]["y"] # type: ignore
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,41 @@

class BlendOverlayPosition(Enum):
TOP_LEFT = "top_left"
TOP_CENTERED = "top_centered"
TOP = "top_centered"
TOP_RIGHT = "top_right"
CENTERED_LEFT = "centered_left"
CENTERED = "centered"
CENTERED_RIGHT = "centered_right"
LEFT = "centered_left"
CENTER = "centered"
RIGHT = "centered_right"
BOTTOM_LEFT = "bottom_left"
BOTTOM_CENTERED = "bottom_centered"
BOTTOM = "bottom_centered"
BOTTOM_RIGHT = "bottom_right"
PERCENT_OFFSET = "percent_offset"
PIXEL_OFFSET = "pixel_offset"


BLEND_OVERLAY_POSITION_LABELS = {
BlendOverlayPosition.TOP_LEFT: "Top left",
BlendOverlayPosition.TOP_CENTERED: "Top centered",
BlendOverlayPosition.TOP_RIGHT: "Top right",
BlendOverlayPosition.CENTERED_LEFT: "Centered left",
BlendOverlayPosition.CENTERED: "Centered",
BlendOverlayPosition.CENTERED_RIGHT: "Centered right",
BlendOverlayPosition.BOTTOM_LEFT: "Bottom left",
BlendOverlayPosition.BOTTOM_CENTERED: "Bottom centered",
BlendOverlayPosition.BOTTOM_RIGHT: "Bottom right",
BlendOverlayPosition.TOP_LEFT: "Top Left",
BlendOverlayPosition.TOP: "Top",
BlendOverlayPosition.TOP_RIGHT: "Top Right",
BlendOverlayPosition.LEFT: "Left",
BlendOverlayPosition.CENTER: "Center",
BlendOverlayPosition.RIGHT: "Right",
BlendOverlayPosition.BOTTOM_LEFT: "Bottom Left",
BlendOverlayPosition.BOTTOM: "Bottom",
BlendOverlayPosition.BOTTOM_RIGHT: "Bottom Right",
BlendOverlayPosition.PERCENT_OFFSET: "Offset (%)",
BlendOverlayPosition.PIXEL_OFFSET: "Offset (pixels)",
}

BLEND_OVERLAY_X0_Y0_FACTORS = {
BlendOverlayPosition.TOP_LEFT: np.array([0, 0]),
BlendOverlayPosition.TOP_CENTERED: np.array([0.5, 0]),
BlendOverlayPosition.TOP: np.array([0.5, 0]),
BlendOverlayPosition.TOP_RIGHT: np.array([1, 0]),
BlendOverlayPosition.CENTERED_LEFT: np.array([0, 0.5]),
BlendOverlayPosition.CENTERED: np.array([0.5, 0.5]),
BlendOverlayPosition.CENTERED_RIGHT: np.array([1, 0.5]),
BlendOverlayPosition.LEFT: np.array([0, 0.5]),
BlendOverlayPosition.CENTER: np.array([0.5, 0.5]),
BlendOverlayPosition.RIGHT: np.array([1, 0.5]),
BlendOverlayPosition.BOTTOM_LEFT: np.array([0, 1]),
BlendOverlayPosition.BOTTOM_CENTERED: np.array([0.5, 1]),
BlendOverlayPosition.BOTTOM: np.array([0.5, 1]),
BlendOverlayPosition.BOTTOM_RIGHT: np.array([1, 1]),
BlendOverlayPosition.PERCENT_OFFSET: np.array([1, 1]),
BlendOverlayPosition.PIXEL_OFFSET: np.array([0, 0]),
Expand All @@ -81,7 +81,7 @@ class BlendOverlayPosition(Enum):
BlendOverlayPosition,
label="Overlay position",
option_labels=BLEND_OVERLAY_POSITION_LABELS,
default=BlendOverlayPosition.CENTERED,
default=BlendOverlayPosition.CENTER,
),
if_enum_group(3, (BlendOverlayPosition.PERCENT_OFFSET))(
SliderInput("X offset", min=-200, max=200, default=0, unit="%"),
Expand Down
2 changes: 1 addition & 1 deletion src/common/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface InputOption {
readonly condition?: Condition | null;
}
export type FileInputKind = 'image' | 'pth' | 'pt' | 'video' | 'bin' | 'param' | 'onnx';
export type DropDownStyle = 'dropdown' | 'checkbox' | 'tabs' | 'icons';
export type DropDownStyle = 'dropdown' | 'checkbox' | 'tabs' | 'icons' | 'anchor';
export interface DropdownGroup {
readonly label?: string | null;
readonly startAt: InputSchemaValue;
Expand Down
7 changes: 6 additions & 1 deletion src/renderer/components/groups/FromToDropdownsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IoMdArrowForward } from 'react-icons/io';
import { Input, InputData, InputId, InputValue, OfKind } from '../../../common/common-types';
import { getInputValue } from '../../../common/util';
import { getPassthroughIgnored } from '../../helpers/nodeState';
import { useValidDropDownValue } from '../../hooks/useValidDropDownValue';
import { DropDown } from '../inputs/elements/Dropdown';
import { InputContainer } from '../inputs/InputContainer';
import { GroupProps } from './props';
Expand All @@ -15,11 +16,15 @@ interface SmallDropDownProps {
isLocked: boolean;
}
const SmallDropDown = memo(({ input, inputData, setInputValue, isLocked }: SmallDropDownProps) => {
const value = getInputValue<string | number>(input.id, inputData);
const setValue = useCallback(
(data?: string | number) => setInputValue(input.id, data ?? input.def),
[setInputValue, input]
);
const value = useValidDropDownValue(
getInputValue<string | number>(input.id, inputData),
setValue,
input
);

return (
<Box w="full">
Expand Down
29 changes: 22 additions & 7 deletions src/renderer/components/groups/MenuIconRowGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { Box } from '@chakra-ui/react';
import { memo } from 'react';
import { DropDownInput, InputSchemaValue } from '../../../common/common-types';
import { getUniqueKey } from '../../../common/group-inputs';
import { getPassthroughIgnored } from '../../helpers/nodeState';
import { NodeState, getPassthroughIgnored } from '../../helpers/nodeState';
import { useValidDropDownValue } from '../../hooks/useValidDropDownValue';
import { IconList } from '../inputs/elements/IconList';
import { InputContainer, WithoutLabel } from '../inputs/InputContainer';
import { IconSet } from './IconSetGroup';
import { GroupProps } from './props';

const IconListWrapper = memo(
({ input, nodeState }: { input: DropDownInput; nodeState: NodeState }) => {
const setValue = (value: InputSchemaValue) => nodeState.setInputValue(input.id, value);
const value = useValidDropDownValue(nodeState.inputData[input.id], setValue, input);

return (
<IconList
isDisabled={nodeState.isLocked}
options={input.options}
value={value}
onChange={setValue}
/>
);
}
);

export const MenuIconRowGroup = memo(({ inputs, nodeState }: GroupProps<'menu-icon-row'>) => {
return (
<InputContainer passthroughIgnored={getPassthroughIgnored(nodeState)}>
Expand All @@ -30,13 +48,10 @@ export const MenuIconRowGroup = memo(({ inputs, nodeState }: GroupProps<'menu-ic

if (item.kind === 'dropdown' && item.preferredStyle === 'icons') {
return (
<IconList
isDisabled={nodeState.isLocked}
<IconListWrapper
input={item}
key={key}
options={item.options}
reset={() => nodeState.setInputValue(item.id, item.def)}
value={nodeState.inputData[item.id]}
onChange={(value) => nodeState.setInputValue(item.id, value)}
nodeState={nodeState}
/>
);
}
Expand Down
Loading

0 comments on commit ee0b03f

Please sign in to comment.