Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group to display multiple checkboxes as a set of icons #2564

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/src/nodes/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,11 @@ def from_to_dropdowns_group(from_dd: BaseInput, to_dd: BaseInput):
`[From] -> [To]` in the UI.
"""
return group("from-to-dropdowns")(from_dd, to_dd)


def icon_set_group(label: str):
"""
This group causes the given boolean inputs to be displayed as a set of icons instead of
checkboxes. The icons are specified by the `icons` parameter.
"""
return group("icon-set", {"label": label})
3 changes: 2 additions & 1 deletion backend/src/nodes/properties/inputs/generic_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def wrap_with_conditional_group(self):


class BoolInput(DropDownInput):
def __init__(self, label: str, default: bool = True):
def __init__(self, label: str, default: bool = True, icon: str | None = None):
super().__init__(
input_type="bool",
label=label,
Expand All @@ -150,6 +150,7 @@ def __init__(self, label: str, default: bool = True):
"option": "Yes",
"value": int(True), # 1
"type": "true",
"icon": icon,
},
{
"option": "No",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np
from PIL import Image, ImageDraw, ImageFont

from nodes.groups import icon_set_group
from nodes.impl.caption import get_font_size
from nodes.impl.color.color import Color
from nodes.impl.image_utils import normalize, to_uint8
Expand All @@ -32,7 +33,7 @@

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


Expand Down Expand Up @@ -98,47 +99,44 @@ class TextAsImagePosition(Enum):
icon="MdTextFields",
inputs=[
TextInput("Text", multiline=True, label_style="hidden"),
BoolInput("Bold", default=False),
BoolInput("Italic", default=False),
ColorInput(channels=[3], default=Color.bgr((0, 0, 0))),
icon_set_group("Style")(
BoolInput("Bold", default=False, icon="FaBold").with_id(1),
BoolInput("Italic", default=False, icon="FaItalic").with_id(2),
),
EnumInput(
TextAsImageAlignment,
label="Alignment",
preferred_style="icons",
option_labels={
TextAsImageAlignment.LEFT: "Left",
TextAsImageAlignment.CENTERED: "Center",
TextAsImageAlignment.RIGHT: "Right",
},
icons={
TextAsImageAlignment.LEFT: "FaAlignLeft",
TextAsImageAlignment.CENTERED: "FaAlignCenter",
TextAsImageAlignment.CENTER: "FaAlignCenter",
TextAsImageAlignment.RIGHT: "FaAlignRight",
},
default=TextAsImageAlignment.CENTERED,
),
default=TextAsImageAlignment.CENTER,
).with_id(4),
ColorInput(channels=[3], default=Color.bgr((0, 0, 0))).with_id(3),
NumberInput(
"Width",
minimum=1,
maximum=None,
controls_step=1,
precision=0,
default=500,
),
).with_id(5),
NumberInput(
"Height",
minimum=1,
maximum=None,
controls_step=1,
precision=0,
default=100,
),
).with_id(6),
EnumInput(
TextAsImagePosition,
label="Position",
option_labels=TEXT_AS_IMAGE_POSITION_LABELS,
default=TextAsImagePosition.CENTERED,
),
).with_id(7),
],
outputs=[
ImageOutput(
Expand All @@ -157,8 +155,8 @@ def text_as_image_node(
text: str,
bold: bool,
italic: bool,
color: Color,
alignment: TextAsImageAlignment,
color: Color,
width: int,
height: int,
position: TextAsImagePosition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np

from nodes.groups import icon_set_group
from nodes.impl.image_utils import as_3d
from nodes.properties.inputs import BoolInput, ImageInput, SliderInput
from nodes.properties.outputs import ImageOutput
Expand All @@ -17,10 +18,12 @@
icon="MdOutlineColorLens",
inputs=[
ImageInput(channels=[1, 3, 4]),
BoolInput("Red", default=True),
BoolInput("Green", default=True),
BoolInput("Blue", default=True),
BoolInput("Alpha", default=False),
icon_set_group("Channels")(
BoolInput("Red", default=True),
BoolInput("Green", default=True),
BoolInput("Blue", default=True),
BoolInput("Alpha", default=False),
),
SliderInput(
"In Black",
minimum=0,
Expand Down
9 changes: 8 additions & 1 deletion src/common/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ interface LinkedInputsGroup extends GroupBase {
readonly kind: 'linked-inputs';
readonly options: Readonly<Record<string, never>>;
}
interface IconSetGroup extends GroupBase {
readonly kind: 'icon-set';
readonly options: {
readonly label: string;
};
}
export type GroupKind = Group['kind'];
export type Group =
| NcnnFileInputGroup
Expand All @@ -246,7 +252,8 @@ export type Group =
| ConditionalGroup
| RequiredGroup
| SeedGroup
| LinkedInputsGroup;
| LinkedInputsGroup
| IconSetGroup;

export type OfKind<T extends { readonly kind: string }, Kind extends T['kind']> = T extends {
readonly kind: Kind;
Expand Down
11 changes: 11 additions & 0 deletions src/common/group-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type DeclaredGroupInputs = InputGuarantees<{
'optional-list': readonly [InputItem, ...InputItem[]];
seed: readonly [NumberInput];
'linked-inputs': readonly [Input, Input, ...Input[]];
'icon-set': readonly DropDownInput[];
}>;

// A bit hacky, but this ensures that GroupInputs covers exactly all group types, no more and no less
Expand Down Expand Up @@ -165,6 +166,16 @@ const groupInputsChecks: {
}
}
},
'icon-set': (inputs) => {
if (inputs.length < 1) return 'Expected at least at one input';

if (
!allInputsOfKind(inputs, 'dropdown') ||
!inputs.every((input) => !input.hasHandle && input.preferredStyle === 'checkbox')
) {
return `Expected all inputs to checkboxes`;
}
},
};

export const checkGroupInputs = (
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/components/CustomIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import { memo } from 'react';
import { IconType } from 'react-icons';
import * as bs from 'react-icons/bs';
import * as cg from 'react-icons/cg';
import { FaAlignCenter, FaAlignLeft, FaAlignRight, FaPaintBrush } from 'react-icons/fa';
import {
FaAlignCenter,
FaAlignLeft,
FaAlignRight,
FaBold,
FaItalic,
FaPaintBrush,
} from 'react-icons/fa';
import { GiRolledCloth } from 'react-icons/gi';
import * as im from 'react-icons/im';
import * as md from 'react-icons/md';

const fa = { FaPaintBrush, FaAlignCenter, FaAlignLeft, FaAlignRight };
const fa = { FaPaintBrush, FaAlignCenter, FaAlignLeft, FaAlignRight, FaBold, FaItalic };
const gi = { GiRolledCloth };

const libraries: Partial<Record<string, Partial<Record<string, IconType>>>> = {
Expand Down Expand Up @@ -160,6 +167,7 @@ export const IconFactory = memo(({ icon, accentColor, boxSize = 4 }: IconFactory
verticalAlign: 'middle',
textRendering: 'geometricPrecision',
fontFamily: 'Noto Emoji, Open Sans, sans-serif',
cursor: 'inherit',
}}
>
{icon}
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/groups/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { InputItem } from '../../../common/group-inputs';
import { NodeState } from '../../helpers/nodeState';
import { ConditionalGroup } from './ConditionalGroup';
import { FromToDropdownsGroup } from './FromToDropdownsGroup';
import { IconSetGroup } from './IconSetGroup';
import { LinkedInputsGroup } from './LinkedInputsGroup';
import { NcnnFileInputsGroup } from './NcnnFileInputsGroup';
import { OptionalInputsGroup } from './OptionalInputsGroup';
Expand All @@ -21,6 +22,7 @@ const GroupComponents: {
'optional-list': OptionalInputsGroup,
seed: SeedGroup,
'linked-inputs': LinkedInputsGroup,
'icon-set': IconSetGroup,
};

interface GroupElementProps {
Expand Down
115 changes: 115 additions & 0 deletions src/renderer/components/groups/IconSetGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Button, ButtonGroup, Tooltip } from '@chakra-ui/react';
import { memo, useEffect } from 'react';
import { DropDownInput, InputOption, InputSchemaValue } from '../../../common/common-types';
import { NodeState } from '../../helpers/nodeState';
import { IconFactory } from '../CustomIcons';
import { InlineLabel, InputContainer } from '../inputs/InputContainer';
import { GroupProps } from './props';

interface SingleIconProps {
value: InputSchemaValue | undefined;
onChange: (value: InputSchemaValue) => void;
reset: () => void;
isDisabled?: boolean;
yes: InputOption;
no: InputOption;
label: string;
leftIsSelected: boolean;
}

const SingleIcon = memo(
({ value, onChange, reset, isDisabled, yes, no, label, leftIsSelected }: SingleIconProps) => {
// reset invalid values to default
useEffect(() => {
if (value === undefined || (yes.value !== value && no.value !== value)) {
reset();
}
}, [value, reset, yes, no]);

const selected = value === yes.value;

return (
<Tooltip
closeOnClick
closeOnPointerDown
hasArrow
borderRadius={8}
isDisabled={isDisabled}
label={label}
openDelay={500}
placement="top"
>
<Button
border={selected ? '1px solid' : undefined}
borderLeft={leftIsSelected ? 'none' : undefined}
boxSizing="content-box"
height="calc(2rem - 2px)"
isDisabled={isDisabled}
minWidth={0}
px={2}
variant={selected ? 'solid' : 'outline'}
onClick={() => onChange(selected ? no.value : yes.value)}
>
<IconFactory icon={yes.icon ?? label[0]} />
</Button>
</Tooltip>
);
}
);

interface IconSetProps {
inputs: readonly DropDownInput[];
nodeState: NodeState;
}
const IconSet = memo(({ inputs, nodeState }: IconSetProps) => {
const { inputData, setInputValue, isLocked } = nodeState;

return (
<ButtonGroup
isAttached
className="nodrag"
variant="outline"
>
{inputs.map((input, i) => {
const value = inputData[input.id];

let leftIsSelected = false;
if (i > 0) {
const rightInput = inputs[i - 1];
const rightValue = inputData[rightInput.id];
const rightYes = rightInput.options[0];
leftIsSelected = rightValue === rightYes.value;
}

return (
<SingleIcon
isDisabled={isLocked}
key={input.id}
label={input.label}
leftIsSelected={leftIsSelected}
no={input.options[1]}
reset={() => setInputValue(input.id, input.def)}
value={value}
yes={input.options[0]}
onChange={(v) => setInputValue(input.id, v)}
/>
);
})}
</ButtonGroup>
);
});

export const IconSetGroup = memo(({ inputs, nodeState, group }: GroupProps<'icon-set'>) => {
const { label } = group.options;

return (
<InputContainer>
<InlineLabel input={{ label }}>
<IconSet
inputs={inputs}
nodeState={nodeState}
/>
</InlineLabel>
</InputContainer>
);
});
Loading
Loading