From 5f3995b983ca131fea311bcb6d09707e8118917b Mon Sep 17 00:00:00 2001 From: Exidex <16986685+Exidex@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:44:34 +0100 Subject: [PATCH] Implement gui for most of the details view --- js/api/gen/components.tsx | 159 ++++++++++++++++++---------- js/api/generator/index.ts | 71 ++++++++++++- js/dev_plugin/src/view.tsx | 66 +++++++++--- rust/client/build.rs | 13 +++ rust/client/src/ui/mod.rs | 31 ++++-- rust/client/src/ui/theme.rs | 18 +++- rust/client/src/ui/widget.rs | 182 +++++++++++++++++++++++++++----- rust/component_model/src/lib.rs | 164 ++++++++++++++++++---------- rust/server/src/plugins/js.rs | 26 ++++- 9 files changed, 548 insertions(+), 182 deletions(-) diff --git a/js/api/gen/components.tsx b/js/api/gen/components.tsx index a8f26a1..fc22174 100644 --- a/js/api/gen/components.tsx +++ b/js/api/gen/components.tsx @@ -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; + label: string; + }; + ["gauntlet:metadata_separator"]: {}; + ["gauntlet:metadata_value"]: { + children?: StringComponent; + label: string; + }; + ["gauntlet:metadata_icon"]: { + icon: string; + label: string; + }; + ["gauntlet:metadata"]: { + children?: ElementComponent; }; ["gauntlet:link"]: { children?: StringComponent; href: string; }; - ["gauntlet:tag"]: { - children?: StringComponent; - color?: string; - onClick?: () => void; - }; - ["gauntlet:metadata_item"]: { - children?: Component; - }; - ["gauntlet:separator"]: {}; - ["gauntlet:metadata"]: { - children?: Component; - }; ["gauntlet:image"]: {}; ["gauntlet:h1"]: { children?: StringComponent; @@ -48,11 +58,14 @@ declare global { ["gauntlet:code"]: { children?: StringComponent; }; + ["gauntlet:paragraph"]: { + children?: StringOrElementComponent; + }; ["gauntlet:content"]: { - children?: Component; + children?: ElementComponent; }; ["gauntlet:detail"]: { - children?: Component; + children?: ElementComponent; }; } } @@ -61,14 +74,68 @@ export type ElementParams> = Comp extends FC export type Element> = ReactElement, JSXElementConstructor>>; export type StringNode = string | number; export type EmptyNode = boolean | null | undefined; -export type Component> = Element | EmptyNode | Iterable>; +export type ElementComponent> = Element | EmptyNode | Iterable>; export type StringComponent = StringNode | EmptyNode | Iterable; -export interface TextProps { +export type StringOrElementComponent> = StringNode | EmptyNode | Element | Iterable>; +export interface MetadataLinkProps { children?: StringComponent; + label: string; + href: string; } -export const Text: FC = (props: TextProps): ReactNode => { - return ; +export const MetadataLink: FC = (props: MetadataLinkProps): ReactNode => { + return ; }; +export interface MetadataTagProps { + children?: StringComponent; + onClick?: () => void; +} +export const MetadataTag: FC = (props: MetadataTagProps): ReactNode => { + return ; +}; +export interface MetadataTagsProps { + children?: ElementComponent; + label: string; +} +export const MetadataTags: FC & { + Tag: typeof MetadataTag; +} = (props: MetadataTagsProps): ReactNode => { + return ; +}; +MetadataTags.Tag = MetadataTag; +export const MetadataSeparator: FC = (): ReactNode => { + return ; +}; +export interface MetadataValueProps { + children?: StringComponent; + label: string; +} +export const MetadataValue: FC = (props: MetadataValueProps): ReactNode => { + return ; +}; +export interface MetadataIconProps { + icon: string; + label: string; +} +export const MetadataIcon: FC = (props: MetadataIconProps): ReactNode => { + return ; +}; +export interface MetadataProps { + children?: ElementComponent; +} +export const Metadata: FC & { + Tags: typeof MetadataTags; + Link: typeof MetadataLink; + Value: typeof MetadataValue; + Icon: typeof MetadataIcon; + Separator: typeof MetadataSeparator; +} = (props: MetadataProps): ReactNode => { + return ; +}; +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 = (props: LinkProps): ReactNode => { return ; }; -export interface TagProps { - children?: StringComponent; - color?: string; - onClick?: () => void; -} -export const Tag: FC = (props: TagProps): ReactNode => { - return ; -}; -export interface MetadataItemProps { - children?: Component; -} -export const MetadataItem: FC & { - Text: typeof Text; - Link: typeof Link; - Tag: typeof Tag; -} = (props: MetadataItemProps): ReactNode => { - return ; -}; -MetadataItem.Text = Text; -MetadataItem.Link = Link; -MetadataItem.Tag = Tag; -export const Separator: FC = (): ReactNode => { - return ; -}; -export interface MetadataProps { - children?: Component; -} -export const Metadata: FC & { - Item: typeof MetadataItem; - Separator: typeof Separator; -} = (props: MetadataProps): ReactNode => { - return ; -}; -Metadata.Item = MetadataItem; -Metadata.Separator = Separator; export const Image: FC = (): ReactNode => { return ; }; @@ -165,11 +197,22 @@ export interface CodeProps { export const Code: FC = (props: CodeProps): ReactNode => { return ; }; +export interface ParagraphProps { + children?: StringOrElementComponent; +} +export const Paragraph: FC & { + Link: typeof Link; + Code: typeof Code; +} = (props: ParagraphProps): ReactNode => { + return ; +}; +Paragraph.Link = Link; +Paragraph.Code = Code; export interface ContentProps { - children?: Component; + children?: ElementComponent; } export const Content: FC & { - Text: typeof Text; + Paragraph: typeof Paragraph; Link: typeof Link; Image: typeof Image; H1: typeof H1; @@ -184,7 +227,7 @@ export const Content: FC & { } = (props: ContentProps): ReactNode => { return ; }; -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; + children?: ElementComponent; } export const Detail: FC & { Metadata: typeof Metadata; diff --git a/js/api/generator/index.ts b/js/api/generator/index.ts index d1b57fd..18d0937 100644 --- a/js/api/generator/index.ts +++ b/js/api/generator/index.ts @@ -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 => ( diff --git a/js/dev_plugin/src/view.tsx b/js/dev_plugin/src/view.tsx index 3d55fe1..ac20d70 100644 --- a/js/dev_plugin/src/view.tsx +++ b/js/dev_plugin/src/view.tsx @@ -15,13 +15,13 @@ export default function View(): ReactElement { H4 Title H5 Title H6 Title - - - s - + Code code Code + + Google Link + Code block Test - - You clicked + + You clicked {count} times {true} {false} {count} @@ -29,26 +29,58 @@ export default function View(): ReactElement { {undefined} {null} {upperCase("times")} - + + Another H4 Title + + 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. + + + 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. + - - Test item text - + { console.log("test " + upperCase("events") + count) setCount(count + 1); }} > Tag - - + + + Another Tag + + - - Test metadata 1 - Test Link - Test metadata 2 - + + Link text + + + 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. + + + + 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. + ); diff --git a/rust/client/build.rs b/rust/client/build.rs index 7856705..e2e8780 100644 --- a/rust/client/build.rs +++ b/rust/client/build.rs @@ -28,6 +28,7 @@ fn main() -> anyhow::Result<()> { if has_children { let string = match children { + Children::StringOrMembers { .. } => "Vec".to_owned(), Children::Members { .. } => "Vec".to_owned(), Children::String { .. } => "Vec".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)); diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index bfe4cda..989ce67 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -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 } } } diff --git a/rust/client/src/ui/theme.rs b/rust/client/src/ui/theme.rs index 29bbcdc..381647a 100644 --- a/rust/client/src/ui/theme.rs +++ b/rust/client/src/ui/theme.rs @@ -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 { diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index 8ac524a..2b3b0f9 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -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 diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index d8e7449..99df5f7 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -79,6 +79,11 @@ pub enum PropertyType { #[derive(Debug, Serialize)] #[serde(tag = "type")] pub enum Children { + #[serde(rename = "string_or_members")] + StringOrMembers { + members: Vec, + component_internal_name: String, + }, #[serde(rename = "members")] Members { members: Vec, @@ -109,6 +114,13 @@ pub struct RootChild { pub component_name: ComponentName, } +fn children_string_or_members(members: Vec) -> Children { + Children::StringOrMembers { + component_internal_name: "text_part".to_owned(), + members, + } +} + fn children_members(members: Vec) -> Children { Children::Members { members, @@ -180,11 +192,74 @@ fn property(name: impl Into, optional: bool, property_type: PropertyType } pub fn create_component_model() -> Vec { - 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 { 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 { 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", ¶graph_component), member("Link", &link_component), member("Image", &image_component), member("H1", &h1_component), @@ -342,18 +389,22 @@ pub fn create_component_model() -> Vec { // 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 { 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 { horizontal_break_component, code_block_component, code_component, + paragraph_component, content_component, detail_component, diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index db03e9d..d81a4e7 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -736,13 +736,29 @@ fn validate_child(state: &Rc>, 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(_) => (),