Implement gui for most of the details view

This commit is contained in:
Exidex 2023-12-17 19:44:34 +01:00
parent 127d4b5c00
commit 5f3995b983
9 changed files with 548 additions and 182 deletions

View file

@ -3,25 +3,35 @@ import { FC, JSXElementConstructor, ReactElement, ReactNode } from "react";
declare global {
namespace JSX {
interface IntrinsicElements {
["gauntlet:text"]: {
["gauntlet:metadata_link"]: {
children?: StringComponent;
label: string;
href: string;
};
["gauntlet:metadata_tag"]: {
children?: StringComponent;
onClick?: () => void;
};
["gauntlet:metadata_tags"]: {
children?: ElementComponent<typeof MetadataTag>;
label: string;
};
["gauntlet:metadata_separator"]: {};
["gauntlet:metadata_value"]: {
children?: StringComponent;
label: string;
};
["gauntlet:metadata_icon"]: {
icon: string;
label: string;
};
["gauntlet:metadata"]: {
children?: ElementComponent<typeof MetadataTags | typeof MetadataLink | typeof MetadataValue | typeof MetadataIcon | typeof MetadataSeparator>;
};
["gauntlet:link"]: {
children?: StringComponent;
href: string;
};
["gauntlet:tag"]: {
children?: StringComponent;
color?: string;
onClick?: () => void;
};
["gauntlet:metadata_item"]: {
children?: Component<typeof Text | typeof Link | typeof Tag>;
};
["gauntlet:separator"]: {};
["gauntlet:metadata"]: {
children?: Component<typeof MetadataItem | typeof Separator>;
};
["gauntlet:image"]: {};
["gauntlet:h1"]: {
children?: StringComponent;
@ -48,11 +58,14 @@ declare global {
["gauntlet:code"]: {
children?: StringComponent;
};
["gauntlet:paragraph"]: {
children?: StringOrElementComponent<typeof Link | typeof Code>;
};
["gauntlet:content"]: {
children?: Component<typeof Text | typeof Link | typeof Image | typeof H1 | typeof H2 | typeof H3 | typeof H4 | typeof H5 | typeof H6 | typeof HorizontalBreak | typeof CodeBlock | typeof Code>;
children?: ElementComponent<typeof Paragraph | typeof Link | typeof Image | typeof H1 | typeof H2 | typeof H3 | typeof H4 | typeof H5 | typeof H6 | typeof HorizontalBreak | typeof CodeBlock | typeof Code>;
};
["gauntlet:detail"]: {
children?: Component<typeof Metadata | typeof Content>;
children?: ElementComponent<typeof Metadata | typeof Content>;
};
}
}
@ -61,14 +74,68 @@ export type ElementParams<Comp extends FC<any>> = Comp extends FC<infer Params>
export type Element<Comp extends FC<any>> = ReactElement<ElementParams<Comp>, JSXElementConstructor<ElementParams<Comp>>>;
export type StringNode = string | number;
export type EmptyNode = boolean | null | undefined;
export type Component<Comp extends FC<any>> = Element<Comp> | EmptyNode | Iterable<Component<Comp>>;
export type ElementComponent<Comp extends FC<any>> = Element<Comp> | EmptyNode | Iterable<ElementComponent<Comp>>;
export type StringComponent = StringNode | EmptyNode | Iterable<StringComponent>;
export interface TextProps {
export type StringOrElementComponent<Comp extends FC<any>> = StringNode | EmptyNode | Element<Comp> | Iterable<StringOrElementComponent<Comp>>;
export interface MetadataLinkProps {
children?: StringComponent;
label: string;
href: string;
}
export const Text: FC<TextProps> = (props: TextProps): ReactNode => {
return <gauntlet:text children={props.children}/>;
export const MetadataLink: FC<MetadataLinkProps> = (props: MetadataLinkProps): ReactNode => {
return <gauntlet:metadata_link children={props.children} label={props.label} href={props.href}/>;
};
export interface MetadataTagProps {
children?: StringComponent;
onClick?: () => void;
}
export const MetadataTag: FC<MetadataTagProps> = (props: MetadataTagProps): ReactNode => {
return <gauntlet:metadata_tag children={props.children} onClick={props.onClick}/>;
};
export interface MetadataTagsProps {
children?: ElementComponent<typeof MetadataTag>;
label: string;
}
export const MetadataTags: FC<MetadataTagsProps> & {
Tag: typeof MetadataTag;
} = (props: MetadataTagsProps): ReactNode => {
return <gauntlet:metadata_tags children={props.children} label={props.label}/>;
};
MetadataTags.Tag = MetadataTag;
export const MetadataSeparator: FC = (): ReactNode => {
return <gauntlet:metadata_separator />;
};
export interface MetadataValueProps {
children?: StringComponent;
label: string;
}
export const MetadataValue: FC<MetadataValueProps> = (props: MetadataValueProps): ReactNode => {
return <gauntlet:metadata_value children={props.children} label={props.label}/>;
};
export interface MetadataIconProps {
icon: string;
label: string;
}
export const MetadataIcon: FC<MetadataIconProps> = (props: MetadataIconProps): ReactNode => {
return <gauntlet:metadata_icon icon={props.icon} label={props.label}/>;
};
export interface MetadataProps {
children?: ElementComponent<typeof MetadataTags | typeof MetadataLink | typeof MetadataValue | typeof MetadataIcon | typeof MetadataSeparator>;
}
export const Metadata: FC<MetadataProps> & {
Tags: typeof MetadataTags;
Link: typeof MetadataLink;
Value: typeof MetadataValue;
Icon: typeof MetadataIcon;
Separator: typeof MetadataSeparator;
} = (props: MetadataProps): ReactNode => {
return <gauntlet:metadata children={props.children}/>;
};
Metadata.Tags = MetadataTags;
Metadata.Link = MetadataLink;
Metadata.Value = MetadataValue;
Metadata.Icon = MetadataIcon;
Metadata.Separator = MetadataSeparator;
export interface LinkProps {
children?: StringComponent;
href: string;
@ -76,41 +143,6 @@ export interface LinkProps {
export const Link: FC<LinkProps> = (props: LinkProps): ReactNode => {
return <gauntlet:link children={props.children} href={props.href}/>;
};
export interface TagProps {
children?: StringComponent;
color?: string;
onClick?: () => void;
}
export const Tag: FC<TagProps> = (props: TagProps): ReactNode => {
return <gauntlet:tag children={props.children} color={props.color} onClick={props.onClick}/>;
};
export interface MetadataItemProps {
children?: Component<typeof Text | typeof Link | typeof Tag>;
}
export const MetadataItem: FC<MetadataItemProps> & {
Text: typeof Text;
Link: typeof Link;
Tag: typeof Tag;
} = (props: MetadataItemProps): ReactNode => {
return <gauntlet:metadata_item children={props.children}/>;
};
MetadataItem.Text = Text;
MetadataItem.Link = Link;
MetadataItem.Tag = Tag;
export const Separator: FC = (): ReactNode => {
return <gauntlet:separator />;
};
export interface MetadataProps {
children?: Component<typeof MetadataItem | typeof Separator>;
}
export const Metadata: FC<MetadataProps> & {
Item: typeof MetadataItem;
Separator: typeof Separator;
} = (props: MetadataProps): ReactNode => {
return <gauntlet:metadata children={props.children}/>;
};
Metadata.Item = MetadataItem;
Metadata.Separator = Separator;
export const Image: FC = (): ReactNode => {
return <gauntlet:image />;
};
@ -165,11 +197,22 @@ export interface CodeProps {
export const Code: FC<CodeProps> = (props: CodeProps): ReactNode => {
return <gauntlet:code children={props.children}/>;
};
export interface ParagraphProps {
children?: StringOrElementComponent<typeof Link | typeof Code>;
}
export const Paragraph: FC<ParagraphProps> & {
Link: typeof Link;
Code: typeof Code;
} = (props: ParagraphProps): ReactNode => {
return <gauntlet:paragraph children={props.children}/>;
};
Paragraph.Link = Link;
Paragraph.Code = Code;
export interface ContentProps {
children?: Component<typeof Text | typeof Link | typeof Image | typeof H1 | typeof H2 | typeof H3 | typeof H4 | typeof H5 | typeof H6 | typeof HorizontalBreak | typeof CodeBlock | typeof Code>;
children?: ElementComponent<typeof Paragraph | typeof Link | typeof Image | typeof H1 | typeof H2 | typeof H3 | typeof H4 | typeof H5 | typeof H6 | typeof HorizontalBreak | typeof CodeBlock | typeof Code>;
}
export const Content: FC<ContentProps> & {
Text: typeof Text;
Paragraph: typeof Paragraph;
Link: typeof Link;
Image: typeof Image;
H1: typeof H1;
@ -184,7 +227,7 @@ export const Content: FC<ContentProps> & {
} = (props: ContentProps): ReactNode => {
return <gauntlet:content children={props.children}/>;
};
Content.Text = Text;
Content.Paragraph = Paragraph;
Content.Link = Link;
Content.Image = Image;
Content.H1 = H1;
@ -197,7 +240,7 @@ Content.HorizontalBreak = HorizontalBreak;
Content.CodeBlock = CodeBlock;
Content.Code = Code;
export interface DetailProps {
children?: Component<typeof Metadata | typeof Content>;
children?: ElementComponent<typeof Metadata | typeof Content>;
}
export const Detail: FC<DetailProps> & {
Metadata: typeof Metadata;

View file

@ -28,12 +28,17 @@ type Property = {
type: PropertyType
}
type PropertyType = TypeString | TypeNumber | TypeBoolean | TypeArray | TypeFunction
type Children = ChildrenMembers | ChildrenString | ChildrenNone
type Children = ChildrenMembers | ChildrenString | ChildrenNone | ChildrenStringOrMembers
type ChildrenMembers = {
type: "members",
members: ChildrenMember[]
}
type ChildrenStringOrMembers = {
type: "string_or_members",
component_internal_name: string,
members: ChildrenMember[]
}
type ChildrenString = {
type: "string"
component_internal_name: string,
@ -209,7 +214,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
),
ts.factory.createTypeAliasDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier("Component"),
ts.factory.createIdentifier("ElementComponent"),
[ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier("Comp"),
@ -234,7 +239,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Iterable"),
[ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Component"),
ts.factory.createIdentifier("ElementComponent"),
[ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Comp"),
undefined
@ -264,6 +269,46 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
)]
)
])
),
ts.factory.createTypeAliasDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier("StringOrElementComponent"),
[ts.factory.createTypeParameterDeclaration(
undefined,
ts.factory.createIdentifier("Comp"),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("FC"),
[ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)]
),
undefined
)],
ts.factory.createUnionTypeNode([
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("StringNode"),
undefined
),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("EmptyNode"),
undefined
),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Element"),
[ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Comp"),
undefined
)]
),
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Iterable"),
[ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("StringOrElementComponent"),
[ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Comp"),
undefined
)]
)]
)
])
)
];
@ -343,7 +388,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
)
let componentType: ts.TypeReferenceNode | ts.IntersectionTypeNode;
if (component.children.type == "members") {
if (component.children.type == "members" || component.children.type == "string_or_members") {
componentType = ts.factory.createIntersectionTypeNode([
componentFCType,
ts.factory.createTypeLiteralNode(
@ -367,6 +412,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
let memberAssignments: ts.Statement[];
switch (component.children.type) {
case "string_or_members":
case "members": {
memberAssignments = component.children.members.map(member => {
return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(
@ -495,7 +541,22 @@ function makeChildrenType(type: Children): ts.TypeNode {
switch (type.type) {
case "members": {
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("Component"),
ts.factory.createIdentifier("ElementComponent"),
[
ts.factory.createUnionTypeNode(
type.members.map(value => (
ts.factory.createTypeQueryNode(
ts.factory.createIdentifier(value.componentName),
undefined
)
))
)
]
)
}
case "string_or_members": {
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("StringOrElementComponent"),
[
ts.factory.createUnionTypeNode(
type.members.map(value => (

View file

@ -15,13 +15,13 @@ export default function View(): ReactElement {
<Detail.Content.H4>H4 Title</Detail.Content.H4>
<Detail.Content.H5>H5 Title</Detail.Content.H5>
<Detail.Content.H6>H6 Title</Detail.Content.H6>
<Detail.Content.Code></Detail.Content.Code>
<Detail.Content.Image></Detail.Content.Image>
<Detail.Content.Link href={""}>s</Detail.Content.Link>
<Detail.Content.CodeBlock></Detail.Content.CodeBlock>
<Detail.Content.Code>Code code Code</Detail.Content.Code>
<Detail.Content.Image/>
<Detail.Content.Link href={"https://google.com/"}>Google Link</Detail.Content.Link>
<Detail.Content.CodeBlock>Code block Test</Detail.Content.CodeBlock>
<Detail.Content.HorizontalBreak/>
<Detail.Content.Text>
You clicked
<Detail.Content.Paragraph>
You clicked {count} times
{true}
{false}
{count}
@ -29,26 +29,58 @@ export default function View(): ReactElement {
{undefined}
{null}
{upperCase("times")}
</Detail.Content.Text>
</Detail.Content.Paragraph>
<Detail.Content.H4>Another H4 Title</Detail.Content.H4>
<Detail.Content.Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip <Detail.Content.Code> ex ea commodo consequat. </Detail.Content.Code> Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</Detail.Content.Paragraph>
<Detail.Content.Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</Detail.Content.Paragraph>
</Detail.Content>
<Detail.Metadata>
<Detail.Metadata.Item>
<Detail.Metadata.Item.Text>Test item text</Detail.Metadata.Item.Text>
<Detail.Metadata.Item.Tag
<Detail.Metadata.Tags label="Tags 1">
<Detail.Metadata.Tags.Tag
onClick={() => {
console.log("test " + upperCase("events") + count)
setCount(count + 1);
}}
>
Tag
</Detail.Metadata.Item.Tag>
</Detail.Metadata.Item>
</Detail.Metadata.Tags.Tag>
<Detail.Metadata.Tags.Tag>
Another Tag
</Detail.Metadata.Tags.Tag>
</Detail.Metadata.Tags>
<Detail.Metadata.Separator/>
<Detail.Metadata.Item>
<Detail.Metadata.Item.Text>Test metadata 1</Detail.Metadata.Item.Text>
<Detail.Metadata.Item.Link href={""}>Test Link</Detail.Metadata.Item.Link>
<Detail.Metadata.Item.Text>Test metadata 2</Detail.Metadata.Item.Text>
</Detail.Metadata.Item>
<Detail.Metadata.Link label="Test 2" href={""}>
Link text
</Detail.Metadata.Link>
<Detail.Metadata.Value label="Label 3">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</Detail.Metadata.Value>
<Detail.Metadata.Icon label="Label 4" icon="icon"/>
<Detail.Metadata.Value label="Label 5">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute
irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</Detail.Metadata.Value>
</Detail.Metadata>
</Detail>
);

View file

@ -28,6 +28,7 @@ fn main() -> anyhow::Result<()> {
if has_children {
let string = match children {
Children::StringOrMembers { .. } => "Vec<ComponentWidgetWrapper>".to_owned(),
Children::Members { .. } => "Vec<ComponentWidgetWrapper>".to_owned(),
Children::String { .. } => "Vec<ComponentWidgetWrapper>".to_owned(),
Children::None => panic!("cannot create type for Children::None")
@ -144,6 +145,12 @@ fn main() -> anyhow::Result<()> {
output.push_str(" match get_component_widget_type(&child) {\n");
match children {
Children::StringOrMembers { members, component_internal_name } => {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", component_internal_name));
for member in members {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name));
}
}
Children::Members { members } => {
for member in members {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name));
@ -245,6 +252,12 @@ fn main() -> anyhow::Result<()> {
output.push_str(" match get_component_widget_type(new_child) {\n");
match children {
Children::StringOrMembers { members, component_internal_name } => {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", component_internal_name));
for member in members {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name));
}
}
Children::Members { members } => {
for member in members {
output.push_str(&format!(" (\"gauntlet:{}\", _) => (),\n", member.component_internal_name));

View file

@ -1,6 +1,6 @@
use std::sync::{Arc, RwLock as StdRwLock};
use iced::{Application, Command, Event, executor, futures, keyboard, Length, Padding, Subscription, subscription};
use iced::{Application, Command, Event, executor, futures, keyboard, Length, Padding, Size, Subscription, subscription};
use iced::futures::channel::mpsc::Sender;
use iced::futures::SinkExt;
use iced::keyboard::KeyCode;
@ -64,6 +64,8 @@ pub enum AppMsg {
const WINDOW_WIDTH: u32 = 650;
const WINDOW_HEIGHT: u32 = 400;
const SUB_VIEW_WINDOW_WIDTH: u32 = 850;
const SUB_VIEW_WINDOW_HEIGHT: u32 = 500;
pub fn run() {
AppModel::run(Settings {
@ -143,7 +145,7 @@ impl Application for AppModel {
let dbus_client = self.dbus_client.clone();
Command::perform(async move {
let open_view = Command::perform(async move {
let event_view_created = DbusEventViewCreated {
reconciler_mode: "persistent".to_owned(),
view_name: entrypoint_id.to_string(), // TODO what was view_name supposed to be?
@ -154,7 +156,12 @@ impl Application for AppModel {
DbusClient::view_created_signal(signal_context, &plugin_id.to_string(), event_view_created)
.await
.unwrap();
}, |_| AppMsg::Noop)
}, |_| AppMsg::Noop);
Command::batch([
iced::window::resize(Size::new(SUB_VIEW_WINDOW_WIDTH, SUB_VIEW_WINDOW_HEIGHT)),
open_view
])
}
AppMsg::PromptChanged(new_prompt) => {
match self.state.last_mut().expect("state is supposed to always have at least one item") {
@ -197,6 +204,9 @@ impl Application for AppModel {
KeyCode::Escape => {
if self.state.len() <= 1 {
iced::window::close()
} else if self.state.len() == 2 {
self.state.pop();
iced::window::resize(Size::new(WINDOW_WIDTH, WINDOW_HEIGHT))
} else {
self.state.pop();
Command::none()
@ -260,7 +270,7 @@ impl Application for AppModel {
.into();
let element: Element<_> = container(column)
.style(ContainerStyle::ApplicationBackground)
.style(ContainerStyle::Background)
.height(Length::Fixed(WINDOW_HEIGHT as f32))
.width(Length::Fixed(WINDOW_WIDTH as f32))
.into();
@ -277,11 +287,14 @@ impl Application for AppModel {
widget_event,
});
container(container_element)
.style(ContainerStyle::ApplicationBackground)
.height(Length::Fixed(WINDOW_HEIGHT as f32))
.width(Length::Fixed(WINDOW_WIDTH as f32))
.into()
let element: Element<_> = container(container_element)
.style(ContainerStyle::Background)
.height(Length::Fixed(SUB_VIEW_WINDOW_HEIGHT as f32))
.width(Length::Fixed(SUB_VIEW_WINDOW_WIDTH as f32))
.into();
// element.explain(iced::color!(0xFF0000))
element
}
}
}

View file

@ -167,7 +167,8 @@ impl scrollable::StyleSheet for GauntletTheme {
pub enum ContainerStyle {
#[default]
Transparent,
ApplicationBackground,
Background,
Code,
}
impl container::StyleSheet for GauntletTheme {
@ -176,7 +177,7 @@ impl container::StyleSheet for GauntletTheme {
fn appearance(&self, style: &Self::Style) -> container::Appearance {
match style {
ContainerStyle::Transparent => Default::default(),
ContainerStyle::ApplicationBackground => {
ContainerStyle::Background => {
let palette = self.extended_palette();
container::Appearance {
@ -187,6 +188,17 @@ impl container::StyleSheet for GauntletTheme {
border_color: palette.background.weak.color,
}
}
ContainerStyle::Code => {
let palette = self.extended_palette();
container::Appearance {
text_color: None,
background: Some(palette.background.weak.color.into()),
border_radius: 4.0.into(),
border_width: 1.0,
border_color: palette.background.weak.color,
}
}
}
}
}
@ -267,7 +279,7 @@ impl button::StyleSheet for GauntletTheme {
ButtonStyle::Positive => from_pair(palette.success.base),
ButtonStyle::Destructive => from_pair(palette.danger.base),
ButtonStyle::Link => button::Appearance {
text_color: palette.background.base.text,
text_color: palette.background.weak.text,
..appearance
},
ButtonStyle::EntrypointItem => button::Appearance {

View file

@ -1,9 +1,10 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use iced::Font;
use iced::{Font, Length, Padding};
use iced::font::Weight;
use iced::widget::{button, column, row, text};
use iced::widget::{button, column, container, horizontal_rule, row, scrollable, text, tooltip, vertical_rule};
use iced::widget::tooltip::Position;
use zbus::SignalContext;
use common::dbus::DbusEventViewEvent;
@ -11,7 +12,7 @@ use common::model::PluginId;
use crate::dbus::DbusClient;
use crate::model::{NativeUiPropertyValue, NativeUiWidget, NativeUiWidgetId};
use crate::ui::theme::{ButtonStyle, Element};
use crate::ui::theme::{ButtonStyle, ContainerStyle, Element};
#[derive(Clone, Debug)]
pub struct ComponentWidgetWrapper {
@ -87,37 +88,100 @@ impl ComponentWidgetWrapper {
text.into()
}
ComponentWidget::Text { children } => {
row(render_children(children, context))
ComponentWidget::MetadataTag { children, onClick: _ } => {
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.into();
let tag: Element<_> = button(content)
.on_press(ComponentWidgetEvent::TagClick { widget: self.as_native_widget() })
.into();
container(tag)
.padding(Padding::new(5.0))
.into()
}
ComponentWidget::MetadataTags { label, children } => {
let value = row(render_children(children, ComponentRenderContext::None))
.into();
render_metadata_item(label, value)
.into()
}
ComponentWidget::MetadataLink { label, children, href } => {
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.into();
let link: Element<_> = button(content)
.style(ButtonStyle::Link)
.on_press(ComponentWidgetEvent::LinkClick { href: href.to_owned() })
.into();
let content: Element<_> = if href.is_empty() {
link
} else {
tooltip(link, href, Position::Top)
.style(ContainerStyle::Background)
.into()
};
render_metadata_item(label, content)
.into()
}
ComponentWidget::MetadataValue { label, children} => {
let value = row(render_children(children, ComponentRenderContext::None))
.into();
render_metadata_item(label, value)
.into()
}
ComponentWidget::MetadataIcon { label, icon} => {
let value = text(icon).into();
render_metadata_item(label, value)
.into()
}
ComponentWidget::MetadataSeparator => {
let separator: Element<_> = horizontal_rule(1)
.into();
container(separator)
.width(Length::Fill)
.padding(Padding::from([10.0, 0.0]))
.into()
}
ComponentWidget::Metadata { children } => {
let metadata: Element<_> = column(render_children(children, ComponentRenderContext::None))
.into();
scrollable(metadata)
.width(Length::Fill)
.into()
}
ComponentWidget::Paragraph { children } => {
let paragraph: Element<_> = row(render_children(children, context))
.into();
container(paragraph)
.width(Length::Fill)
.padding(Padding::new(5.0))
.into()
}
ComponentWidget::Link { children, href } => {
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.into();
button(content)
let content: Element<_> = button(content)
.style(ButtonStyle::Link)
.on_press(ComponentWidgetEvent::LinkClick { href: href.to_owned() })
.into()
}
ComponentWidget::Tag { children, onClick: _, color: _ } => {
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.into();
button(content)
.on_press(ComponentWidgetEvent::TagClick { widget: self.as_native_widget() })
.into()
}
ComponentWidget::MetadataItem { children } => {
row(render_children(children, ComponentRenderContext::None))
.into()
}
ComponentWidget::Separator => {
text("Separator").into()
}
ComponentWidget::Metadata { children } => {
column(render_children(children, ComponentRenderContext::None))
.into()
if href.is_empty() {
content
} else {
tooltip(content, href, Position::Top)
.style(ContainerStyle::Background)
.into()
}
}
ComponentWidget::Image => {
text("Image").into()
@ -147,25 +211,67 @@ impl ComponentWidgetWrapper {
.into()
}
ComponentWidget::HorizontalBreak => {
text("HorizontalBreak").into()
let separator: Element<_> = horizontal_rule(1).into();
container(separator)
.width(Length::Fill)
.padding(Padding::from([10.0, 0.0]))
.into()
}
ComponentWidget::CodeBlock { children } => {
text("CodeBlock").into()
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.padding(Padding::from([3.0, 5.0]))
.into();
container(content)
.width(Length::Fill)
.style(ContainerStyle::Code)
.into()
}
ComponentWidget::Code { children } => {
text("Code").into()
let content: Element<_> = row(render_children(children, ComponentRenderContext::None))
.padding(Padding::from([3.0, 5.0]))
.into();
container(content)
.style(ContainerStyle::Code)
.into()
}
ComponentWidget::Content { children } => {
column(render_children(children, ComponentRenderContext::None))
let content: Element<_> = column(render_children(children, ComponentRenderContext::None))
.into();
scrollable(content)
// .direction(Direction::Both { horizontal: Properties::default(), vertical: Properties::default() })
.width(Length::Fill)
.into()
}
ComponentWidget::Detail { children } => {
let metadata_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::Metadata { .. }), ComponentRenderContext::None)
.unwrap();
let metadata_element = container(metadata_element)
.width(Length::FillPortion(2))
.padding(Padding::new(5.0))
.into();
let content_element = render_child_by_type(children, |widget| matches!(widget, ComponentWidget::Content { .. }), ComponentRenderContext::None)
.unwrap();
row(vec![content_element, metadata_element])
let content_element = container(content_element)
.width(Length::FillPortion(3))
.padding(Padding::new(5.0))
.into();
let separator = vertical_rule(1)
.into();
let content: Element<_> = row(vec![content_element, separator, metadata_element])
.into();
container(content)
.width(Length::Fill)
.padding(Padding::new(10.0))
.into()
}
ComponentWidget::Root { children } => {
@ -196,6 +302,24 @@ impl ComponentWidgetWrapper {
}
}
fn render_metadata_item<'a>(label: &str, value: Element<'a, ComponentWidgetEvent>) -> Element<'a, ComponentWidgetEvent> {
let bold_font = Font {
weight: Weight::Bold,
..Font::DEFAULT
};
let label: Element<_> = text(label)
.font(bold_font)
.into();
let value = container(value)
.padding(Padding::new(5.0))
.into();
column(vec![label, value])
.into()
}
fn render_children<'a>(
content: &[ComponentWidgetWrapper],
context: ComponentRenderContext

View file

@ -79,6 +79,11 @@ pub enum PropertyType {
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
pub enum Children {
#[serde(rename = "string_or_members")]
StringOrMembers {
members: Vec<ChildrenMember>,
component_internal_name: String,
},
#[serde(rename = "members")]
Members {
members: Vec<ChildrenMember>,
@ -109,6 +114,13 @@ pub struct RootChild {
pub component_name: ComponentName,
}
fn children_string_or_members(members: Vec<ChildrenMember>) -> Children {
Children::StringOrMembers {
component_internal_name: "text_part".to_owned(),
members,
}
}
fn children_members(members: Vec<ChildrenMember>) -> Children {
Children::Members {
members,
@ -180,11 +192,74 @@ fn property(name: impl Into<String>, optional: bool, property_type: PropertyType
}
pub fn create_component_model() -> Vec<Component> {
let text_component = component(
"text",
"Text",
let metadata_link_component = component(
"metadata_link",
"MetadataLink",
vec![
property("label", false, PropertyType::String),
property("href", false, PropertyType::String),
],
children_string()
);
let metadata_tag_component = component(
"metadata_tag",
"MetadataTag",
vec![
// property("color", true, PropertyType::String),
property("onClick", true, PropertyType::Function)
],
children_string()
);
let metadata_tags_component = component(
"metadata_tags",
"MetadataTags",
vec![
property("label", false, PropertyType::String)
],
children_members(vec![
member("Tag", &metadata_tag_component),
])
);
let metadata_separator_component = component(
"metadata_separator",
"MetadataSeparator",
vec![],
children_string(),
children_none()
);
let metadata_icon_component = component(
"metadata_icon",
"MetadataIcon",
vec![
property("icon", false, PropertyType::String),
property("label", false, PropertyType::String),
],
children_none()
);
let metadata_value_component = component(
"metadata_value",
"MetadataValue",
vec![
property("label", false, PropertyType::String),
],
children_string()
);
let metadata_component = component(
"metadata",
"Metadata",
vec![],
children_members(vec![
member("Tags", &metadata_tags_component),
member("Link", &metadata_link_component),
member("Value", &metadata_value_component),
member("Icon", &metadata_icon_component),
member("Separator", &metadata_separator_component),
])
);
let link_component = component(
@ -196,44 +271,6 @@ pub fn create_component_model() -> Vec<Component> {
children_string()
);
let tag_component = component(
"tag",
"Tag",
vec![
property("color", true, PropertyType::String),
property("onClick", true, PropertyType::Function)
],
children_string()
);
let metadata_item_component = component(
"metadata_item",
"MetadataItem",
vec![],
children_members(vec![
member("Text", &text_component),
member("Link", &link_component),
member("Tag", &tag_component),
])
);
let separator_component = component(
"separator",
"Separator",
vec![],
children_none()
);
let metadata_component = component(
"metadata",
"Metadata",
vec![],
children_members(vec![
member("Item", &metadata_item_component),
member("Separator", &separator_component),
])
);
let image_component = component(
"image",
"Image",
@ -306,12 +343,22 @@ pub fn create_component_model() -> Vec<Component> {
children_string()
);
let paragraph_component = component(
"paragraph",
"Paragraph",
vec![],
children_string_or_members(vec![
member("Link", &link_component),
member("Code", &code_component),
]),
);
let content_component = component(
"content",
"Content",
vec![],
children_members(vec![
member("Text", &text_component),
member("Paragraph", &paragraph_component),
member("Link", &link_component),
member("Image", &image_component),
member("H1", &h1_component),
@ -342,18 +389,22 @@ pub fn create_component_model() -> Vec<Component> {
// Detail
// Detail.Content
// Detail.Content.Text
// Detail.Content.Link
// Detail.Content.Code
// Detail.Content.Paragraph
// Detail.Content.Paragraph.Link
// Detail.Content.Paragraph.Code
// Detail.Content.Image
// Detail.Content.H1-6
// Detail.Content.HorizontalBreak
// Detail.Content.CodeBlock
// Detail.Content.Code
// Detail.Metadata.Item -- label, icon
// Detail.Metadata.Item.Text
// Detail.Metadata.Item.Link
// Detail.Metadata.Item.Tag
// Detail.Metadata
// Detail.Metadata.Tags
// Detail.Metadata.Tags.Tag
// Detail.Metadata.Separator
// Detail.Metadata.Link
// Detail.Metadata.Value
// Detail.Metadata.Icon
// ActionPanel
// ActionPanel.Section
@ -396,14 +447,14 @@ pub fn create_component_model() -> Vec<Component> {
vec![
text_part,
text_component,
link_component,
tag_component,
metadata_item_component,
separator_component,
metadata_link_component,
metadata_tag_component,
metadata_tags_component,
metadata_separator_component,
metadata_value_component,
metadata_icon_component,
metadata_component,
link_component,
image_component,
h1_component,
h2_component,
@ -414,6 +465,7 @@ pub fn create_component_model() -> Vec<Component> {
horizontal_break_component,
code_block_component,
code_component,
paragraph_component,
content_component,
detail_component,

View file

@ -736,13 +736,29 @@ fn validate_child(state: &Rc<RefCell<OpState>>, parent_internal_name: &str, chil
match parent_component {
Component::Standard { name: parent_name, children: parent_children, .. } => {
match parent_children {
Children::Members { members } => {
let allowed_members: HashMap<_, _> = members.iter()
.map(|member| (&member.component_internal_name, member))
.collect();
Children::StringOrMembers { members, .. } => {
match child_component {
Component::Standard { internal_name, name, .. } => {
let allowed_members: HashMap<_, _> = members.iter()
.map(|member| (&member.component_internal_name, member))
.collect();
match allowed_members.get(internal_name) {
None => Err(anyhow::anyhow!("{} component not be a child of {}", name, parent_name))?,
Some(_) => (),
}
}
Component::Root { .. } => Err(anyhow::anyhow!("root component not be a child"))?,
Component::TextPart { .. } => ()
}
}
Children::Members { members } => {
match child_component {
Component::Standard { internal_name, name, .. } => {
let allowed_members: HashMap<_, _> = members.iter()
.map(|member| (&member.component_internal_name, member))
.collect();
match allowed_members.get(internal_name) {
None => Err(anyhow::anyhow!("{} component not be a child of {}", name, parent_name))?,
Some(_) => (),