diff --git a/dev_plugin/gauntlet.toml b/dev_plugin/gauntlet.toml index 6493338f..7cb232b1 100644 --- a/dev_plugin/gauntlet.toml +++ b/dev_plugin/gauntlet.toml @@ -13,6 +13,12 @@ name = 'Form view' path = 'src/form-view.tsx' type = 'view' +[[entrypoint]] +id = 'inline-view' +name = 'Inline view' +path = 'src/inline-view.tsx' +type = 'inline-view' + [[entrypoint]] id = 'command-a' name = 'Command A' diff --git a/dev_plugin/package-lock.json b/dev_plugin/package-lock.json index 7f0ea3cf..27e7ffb7 100644 --- a/dev_plugin/package-lock.json +++ b/dev_plugin/package-lock.json @@ -15,7 +15,7 @@ "@project-gauntlet/deno": "file:../js/deno", "@project-gauntlet/tools": "file:../../tools", "@types/react": "^18.2.14", - "typescript": "^5.2.2" + "typescript": "^5.3.3" } }, "../../../tools": { @@ -91,8 +91,7 @@ "version": "0.1.0", "devDependencies": { "@project-gauntlet/typings": "*", - "@types/node": "^18.17.1", - "typescript": "^5.2.2" + "typescript": "^5.3.3" } }, "../js/deno": { @@ -101,7 +100,7 @@ "dev": true, "devDependencies": { "@types/node": "^18.17.1", - "typescript": "^5.2.2" + "typescript": "^5.3.3" } }, "../tools": { diff --git a/dev_plugin/src/inline-view.tsx b/dev_plugin/src/inline-view.tsx new file mode 100644 index 00000000..fcd0ac45 --- /dev/null +++ b/dev_plugin/src/inline-view.tsx @@ -0,0 +1,24 @@ +import { Content, Inline } from "@project-gauntlet/api/components"; +import { ReactNode } from "react"; + +export default function InlineView(props: { text: string }): ReactNode | undefined { + if (!props.text.startsWith("inline")) { + return undefined + } + + return ( + + + + Testing inline view left {props.text} + + + + + + Testing inline view right + + + + ) +} diff --git a/js/api/src/gen/components.tsx b/js/api/src/gen/components.tsx index ad951e15..982d82e2 100644 --- a/js/api/src/gen/components.tsx +++ b/js/api/src/gen/components.tsx @@ -110,6 +110,10 @@ declare global { ["gauntlet:form"]: { children?: ElementComponent; }; + ["gauntlet:inline_separator"]: {}; + ["gauntlet:inline"]: { + children?: ElementComponent; + }; } } } @@ -383,3 +387,21 @@ Form.Checkbox = Checkbox; Form.DatePicker = DatePicker; Form.Select = Select; Form.Separator = Separator; +export const InlineSeparator: FC = (): ReactNode => { + return ; +}; +export interface InlineProps { + children?: ElementComponent; +} +export const Inline: FC & { + Left: typeof Content; + Separator: typeof InlineSeparator; + Right: typeof Content; + Center: typeof Content; +} = (props: InlineProps): ReactNode => { + return ; +}; +Inline.Left = Content; +Inline.Separator = InlineSeparator; +Inline.Right = Content; +Inline.Center = Content; diff --git a/js/core/rollup.config.ts b/js/core/rollup.config.ts index 4a9f2b2a..68a33d4f 100644 --- a/js/core/rollup.config.ts +++ b/js/core/rollup.config.ts @@ -5,7 +5,7 @@ import { defineConfig } from "rollup"; export default defineConfig({ input: [ - 'src/init.ts', + 'src/init.tsx', ], output: [ { @@ -14,6 +14,7 @@ export default defineConfig({ sourcemap: 'inline', } ], + external: ["react", "react/jsx-runtime"], plugins: [ nodeResolve(), commonjs(), diff --git a/js/core/src/init.ts b/js/core/src/init.tsx similarity index 73% rename from js/core/src/init.ts rename to js/core/src/init.tsx index 2d614d6e..7e9b51d8 100644 --- a/js/core/src/init.ts +++ b/js/core/src/init.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, isValidElement, ReactNode } from "react"; // @ts-expect-error does typescript support such symbol declarations? const denoCore: DenoCore = Deno[Deno.internal].core; @@ -84,9 +84,9 @@ async function runLoop() { } case "OpenView": { try { - const view: FC = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; - const { renderTopmostView } = await import("gauntlet:renderer"); - latestRootUiWidget = renderTopmostView(pluginEvent.frontend, view); + const View: FC = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default; + const { render } = await import("gauntlet:renderer"); + latestRootUiWidget = render(pluginEvent.frontend, "View", ); } catch (e) { console.error("Error occurred when rendering view", pluginEvent.entrypointId, e) } @@ -100,6 +100,28 @@ async function runLoop() { } break; } + case "OpenInlineView": { + const endpoint_id = InternalApi.op_inline_view_endpoint_id(); + + if (endpoint_id) { + try { + const View: FC<{ text: string }> = (await import(`gauntlet:entrypoint?${endpoint_id}`)).default; + const { render } = await import("gauntlet:renderer"); + const renderResult = ; + + if (isValidElement(renderResult)) { + InternalApi.op_log_debug("plugin_loop", "Inline view function returned react component, rendering...") + latestRootUiWidget = render("default", "InlineView", renderResult); + } else { + InternalApi.op_log_debug("plugin_loop", `Inline view function returned ${Deno.inspect(renderResult)}, closing view...`) + InternalApi.clear_inline_view() + } + } catch (e) { + console.error("Error occurred when rendering inline view", e) + } + } + break; + } case "PluginCommand": { switch (pluginEvent.commandType) { case "stop": { diff --git a/js/core/typings/index.d.ts b/js/core/typings/index.d.ts index 3f0fa54c..3b0ac96a 100644 --- a/js/core/typings/index.d.ts +++ b/js/core/typings/index.d.ts @@ -1,6 +1,6 @@ declare module "gauntlet:renderer" { - import { FC } from "react"; + import { ReactNode } from "react"; - const renderTopmostView: (frontend: string, component: FC) => UiWidget; - export { renderTopmostView }; + const render: (frontend: string, renderLocation: RenderLocation, component: ReactNode) => UiWidget; + export { render }; } \ No newline at end of file diff --git a/js/react_renderer/rollup.config.ts b/js/react_renderer/rollup.config.ts index fdfad35b..41587a30 100644 --- a/js/react_renderer/rollup.config.ts +++ b/js/react_renderer/rollup.config.ts @@ -7,7 +7,7 @@ import { defineConfig, RollupOptions } from "rollup"; const config = (nodeEnv: string, outDir: string): RollupOptions => { return { input: [ - 'src/renderer.tsx', + 'src/renderer.ts', ], output: [ { diff --git a/js/react_renderer/src/renderer.tsx b/js/react_renderer/src/renderer.ts similarity index 92% rename from js/react_renderer/src/renderer.tsx rename to js/react_renderer/src/renderer.ts index eafac52b..b5e934ae 100644 --- a/js/react_renderer/src/renderer.tsx +++ b/js/react_renderer/src/renderer.ts @@ -30,37 +30,41 @@ type SuspenseInstance = never; type ChildSet = UiWidget[] class GauntletContextValue { - private navStack: ReactNode[] = [] - root: UiWidget | undefined - rerenderFn: ((node: ReactNode) => void) | undefined - - reset(root: UiWidget, View: FC, rerender: (node: ReactNode) => void) { - this.root = root - this.rerenderFn = rerender - this.navStack = [] - this.navStack.push() + private _navStack: ReactNode[] = [] + private _renderLocation: RenderLocation | undefined + private _rerender: ((node: ReactNode) => void) | undefined + + reset(renderLocation: RenderLocation, view: ReactNode, rerender: (node: ReactNode) => void) { + this._renderLocation = renderLocation + this._rerender = rerender + this._navStack = [] + this._navStack.push(view) + } + + renderLocation(): RenderLocation { + return this._renderLocation!! } isBottommostView() { - return this.navStack.length === 1 + return this._navStack.length === 1 } topmostView() { - return this.navStack[this.navStack.length - 1] + return this._navStack[this._navStack.length - 1] } rerender = (component: ReactNode) => { - this.rerenderFn!!(component) + this._rerender!!(component) }; pushView = (component: ReactNode) => { - this.navStack.push(component) + this._navStack.push(component) this.rerender(component) }; popView = () => { - this.navStack.pop(); + this._navStack.pop(); this.rerender(this.topmostView()) }; @@ -278,7 +282,7 @@ export const createHostConfig = (): HostConfig< replaceContainerChildren(container: RootUiWidget, newChildren: ChildSet): void { InternalApi.op_log_trace("renderer_js_persistence", `replaceContainerChildren is called, container: ${Deno.inspect(container)}, newChildren: ${Deno.inspect(newChildren)}`) container.widgetChildren = newChildren - InternalApi.op_react_replace_container_children(gauntletContextValue.isBottommostView(), container) + InternalApi.op_react_replace_view(gauntletContextValue.renderLocation(), gauntletContextValue.isBottommostView(), container) }, cloneHiddenInstance( @@ -338,7 +342,7 @@ const createTracedHostConfig = (hostConfig: any) => new Proxy(hostConfig, { } }); -export function renderTopmostView(frontend: string, view: FC): UiWidget { +export function render(frontend: string, renderLocation: RenderLocation, view: ReactNode): UiWidget { // specific frontend are implemented separately, it seems it is not feasible to do generic implementation if (frontend !== "default") { throw new Error("NOT SUPPORTED") @@ -356,7 +360,7 @@ export function renderTopmostView(frontend: string, view: FC): UiWidget { widgetChildren: [], }; - gauntletContextValue.reset(container, view, (node: ReactNode) => { + gauntletContextValue.reset(renderLocation, view, (node: ReactNode) => { reconciler.updateContainer( node, root, diff --git a/js/typings/index.d.ts b/js/typings/index.d.ts index da58c1bb..d3b1dc20 100644 --- a/js/typings/index.d.ts +++ b/js/typings/index.d.ts @@ -6,7 +6,8 @@ interface DenoCore { ops: InternalApi } -type PluginEvent = ViewEvent | RunCommand | OpenView | PluginCommand +type PluginEvent = ViewEvent | RunCommand | OpenView | PluginCommand | OpenInlineView +type RenderLocation = "InlineView" | "View" type ViewEvent = { type: "ViewEvent" @@ -20,6 +21,7 @@ type OpenView = { frontend: string entrypointId: string } + type RunCommand = { type: "RunCommand" entrypointId: string @@ -30,6 +32,11 @@ type PluginCommand = { commandType: "stop" } +type OpenInlineView = { + type: "OpenInlineView" + text: string +} + type PropertyValue = PropertyValueString | PropertyValueNumber | PropertyValueBool | PropertyValueUndefined type PropertyValueString = { type: "String", value: string } type PropertyValueNumber = { type: "Number", value: number } @@ -55,7 +62,10 @@ interface InternalApi { op_component_model(): Record; - op_react_replace_container_children(top_level_view: boolean, container: UiWidget): void; + op_inline_view_endpoint_id(): string | null; + clear_inline_view(): void; + + op_react_replace_view(render_location: RenderLocation, top_level_view: boolean, container: UiWidget): void; } // component model types diff --git a/package-lock.json b/package-lock.json index 8b4a9cd0..5cbf582a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,14 @@ "": { "name": "project-gauntlet", "workspaces": [ - "js/*" + "js/typings", + "js/build", + "js/api_build", + "js/deno", + "js/api", + "js/react", + "js/core", + "js/react_renderer" ] }, "js/api": { diff --git a/package.json b/package.json index 008ab84c..6babe361 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,13 @@ "build": "npm run build --workspaces --if-present" }, "workspaces": [ - "js/*" + "js/typings", + "js/build", + "js/api_build", + "js/deno", + "js/api", + "js/react", + "js/core", + "js/react_renderer" ] } diff --git a/rust/client/src/dbus.rs b/rust/client/src/dbus.rs index ed4dc642..cd08b4f8 100644 --- a/rust/client/src/dbus.rs +++ b/rust/client/src/dbus.rs @@ -1,10 +1,10 @@ use zbus::DBusError; -use common::dbus::{DbusEventRenderView, DbusEventRunCommand, DbusEventViewEvent, DBusSearchResult, DBusUiWidget}; +use common::dbus::{DbusEventRenderView, DbusEventRunCommand, DbusEventViewEvent, DBusSearchResult, DBusUiWidget, RenderLocation}; use common::model::PluginId; use utils::channel::RequestSender; -use crate::model::{NativeUiRequestData, NativeUiResponseData, NativeUiWidget}; +use crate::model::{NativeUiRequestData, NativeUiResponseData}; pub struct DbusClient { pub(crate) context_tx: RequestSender<(PluginId, NativeUiRequestData), NativeUiResponseData> @@ -21,19 +21,31 @@ impl DbusClient { #[dbus_interface(signal)] pub async fn view_event_signal(signal_ctxt: &zbus::SignalContext<'_>, plugin_id: &str, event: DbusEventViewEvent) -> zbus::Result<()>; - async fn replace_container_children(&self, plugin_id: &str, top_level_view: bool, container: DBusUiWidget) -> Result<()> { + async fn replace_view(&self, plugin_id: &str, render_location: RenderLocation, top_level_view: bool, container: DBusUiWidget) -> Result<()> { let container = container.try_into() .expect("unable to convert widget into native"); - let data = NativeUiRequestData::ReplaceContainerChildren { top_level_view, container }; + let data = NativeUiRequestData::ReplaceView { render_location, top_level_view, container }; let data = (PluginId::from_string(plugin_id), data); match self.context_tx.send_receive(data).await { - NativeUiResponseData::ReplaceContainerChildren => {}, + NativeUiResponseData::Nothing => {} }; Ok(()) } + + async fn clear_inline_view(&self, plugin_id: &str) -> Result<()> { + let data = NativeUiRequestData::ClearInlineView; + let data = (PluginId::from_string(plugin_id), data); + + match self.context_tx.send_receive(data).await { + NativeUiResponseData::Nothing => {} + }; + + Ok(()) + } + } type Result = core::result::Result; diff --git a/rust/client/src/model.rs b/rust/client/src/model.rs index 3690e0a5..9ce8ddb7 100644 --- a/rust/client/src/model.rs +++ b/rust/client/src/model.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::anyhow; use zbus::zvariant::Value; -use common::dbus::{DBusUiPropertyContainer, DBusUiPropertyValue, DBusUiPropertyValueType, DBusUiWidget}; +use common::dbus::{DBusUiPropertyContainer, DBusUiPropertyValue, DBusUiPropertyValueType, DBusUiWidget, RenderLocation}; use common::model::{EntrypointId, PluginId}; #[derive(Debug, Clone)] @@ -23,12 +23,14 @@ pub enum SearchResultEntrypointType { #[derive(Debug)] pub enum NativeUiResponseData { - ReplaceContainerChildren, + Nothing, } #[derive(Debug)] pub enum NativeUiRequestData { - ReplaceContainerChildren { + ClearInlineView, + ReplaceView { + render_location: RenderLocation, top_level_view: bool, container: NativeUiWidget, }, diff --git a/rust/client/src/ui/client_context.rs b/rust/client/src/ui/client_context.rs new file mode 100644 index 00000000..a580ca11 --- /dev/null +++ b/rust/client/src/ui/client_context.rs @@ -0,0 +1,80 @@ +use std::future::Future; + +use zbus::SignalContext; + +use common::dbus::RenderLocation; +use common::model::PluginId; + +use crate::model::NativeUiWidget; +use crate::ui::widget::ComponentWidgetEvent; +use crate::ui::widget_container::PluginWidgetContainer; + +pub struct ClientContext { + inline_views: Vec<(PluginId, PluginWidgetContainer)>, + view: PluginWidgetContainer, +} + +impl ClientContext { + pub fn new() -> Self { + Self { + inline_views: vec![], + view: PluginWidgetContainer::new(), + } + } + + pub fn get_all_inline_view_containers(&self) -> &Vec<(PluginId, PluginWidgetContainer)> { + &self.inline_views + } + + pub fn get_inline_view_container(&self, plugin_id: &PluginId) -> &PluginWidgetContainer { + self.inline_views.iter() + .find(|(id, _)| id == plugin_id) + .map(|(_, container)| container) + .unwrap() + } + + pub fn get_mut_inline_view_container(&mut self, plugin_id: &PluginId) -> &mut PluginWidgetContainer { + if let Some(index) = self.inline_views.iter().position(|(id, _)| id == plugin_id) { + let (_, container) = &mut self.inline_views[index]; + container + } else { + self.inline_views.push((plugin_id.clone(), PluginWidgetContainer::new())); + let (_, container) = self.inline_views.last_mut().unwrap(); + container + } + } + + pub fn get_view_container(&self) -> &PluginWidgetContainer { + &self.view + } + + pub fn get_mut_view_container(&mut self) -> &mut PluginWidgetContainer { + &mut self.view + } + + pub fn replace_view(&mut self, render_location: RenderLocation, container: NativeUiWidget, plugin_id: &PluginId) { + match render_location { + RenderLocation::InlineView => self.get_mut_inline_view_container(plugin_id).replace_view(container), + RenderLocation::View => self.get_mut_view_container().replace_view(container) + } + } + + pub fn clear_inline_view(&mut self, plugin_id: &PluginId) { + if let Some(index) = self.inline_views.iter().position(|(id, _)| id == plugin_id) { + self.inline_views.remove(index); + } + } + + pub fn handle_event<'a>( + &self, + signal_context: &'a SignalContext<'_>, + render_location: RenderLocation, + plugin_id: &'a PluginId, + event: ComponentWidgetEvent + ) -> impl Future + 'a + Send { + match render_location { + RenderLocation::InlineView => self.get_inline_view_container(&plugin_id).handle_event(signal_context, plugin_id.clone(), event), + RenderLocation::View => self.get_view_container().handle_event(signal_context, plugin_id.clone(), event) + } + } +} diff --git a/rust/client/src/ui/inline_view_container.rs b/rust/client/src/ui/inline_view_container.rs new file mode 100644 index 00000000..4dded78e --- /dev/null +++ b/rust/client/src/ui/inline_view_container.rs @@ -0,0 +1,74 @@ +use std::sync::{Arc, RwLock}; + +use iced::widget::Component; +use iced::widget::component; + +use common::dbus::RenderLocation; + +use crate::ui::AppMsg; +use crate::ui::client_context::ClientContext; +use crate::ui::theme::{Element, GauntletRenderer}; +use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent}; + +pub struct InlineViewContainer { + client_context: Arc>, +} + +pub fn inline_view_container(client_context: Arc>) -> InlineViewContainer { + InlineViewContainer { + client_context, + } +} + +#[derive(Default)] +pub struct PluginContainerState { + current_plugin: usize +} + +pub enum InlineViewContainerEvent { + WidgetEvent(ComponentWidgetEvent), + Local { + + } +} + +impl Component for InlineViewContainer { + type State = PluginContainerState; + type Event = InlineViewContainerEvent; + + fn update( + &mut self, + state: &mut Self::State, + event: Self::Event, + ) -> Option { + match event { + InlineViewContainerEvent::WidgetEvent(event) => { + let client_context = self.client_context.read().expect("lock is poisoned"); + let containers = client_context.get_all_inline_view_containers(); + let (plugin_id, _) = &containers[state.current_plugin]; + + Some(AppMsg::WidgetEvent { + plugin_id: plugin_id.to_owned(), + render_location: RenderLocation::InlineView, + widget_event: event, + }) + } + InlineViewContainerEvent::Local { .. } => Some(AppMsg::Noop) } + } + + fn view(&self, state: &Self::State) -> Element { + let client_context = self.client_context.read().expect("lock is poisoned"); + let containers = client_context.get_all_inline_view_containers(); + + let (_, container) = &containers[state.current_plugin]; + + container.render_widget(ComponentRenderContext::None) + .map(InlineViewContainerEvent::WidgetEvent) + } +} + +impl<'a> From for Element<'a, AppMsg> { + fn from(container: InlineViewContainer) -> Self { + component(container) + } +} \ No newline at end of file diff --git a/rust/client/src/ui/mod.rs b/rust/client/src/ui/mod.rs index 91fcc185..9ed24124 100644 --- a/rust/client/src/ui/mod.rs +++ b/rust/client/src/ui/mod.rs @@ -6,28 +6,33 @@ use iced::futures::channel::mpsc::Sender; use iced::futures::SinkExt; use iced::keyboard::KeyCode; use iced::Settings; -use iced::widget::{column, container, horizontal_rule, scrollable, text_input}; +use iced::widget::{column, container, horizontal_rule, horizontal_space, scrollable, text_input}; use iced::widget::text_input::focus; use iced::window::Position; use iced_aw::graphics::icons; use tokio::sync::RwLock as TokioRwLock; use zbus::{Connection, InterfaceRef}; +use client_context::ClientContext; -use common::dbus::{DBusEntrypointType, DbusEventRenderView, DbusEventRunCommand}; +use common::dbus::{DBusEntrypointType, DbusEventRenderView, DbusEventRunCommand, RenderLocation}; use common::model::{EntrypointId, PluginId}; use utils::channel::{channel, RequestReceiver}; use crate::dbus::{DbusClient, DbusServerProxyProxy}; use crate::model::{NativeUiRequestData, NativeUiResponseData, NativeUiSearchResult, SearchResultEntrypointType}; -use crate::ui::plugin_container::{ClientContext, plugin_container}; +use crate::ui::inline_view_container::inline_view_container; +use crate::ui::view_container::view_container; use crate::ui::search_list::search_list; use crate::ui::theme::{ContainerStyle, Element, GauntletTheme}; use crate::ui::widget::ComponentWidgetEvent; -mod plugin_container; +mod view_container; mod search_list; mod widget; mod theme; +mod client_context; +mod widget_container; +mod inline_view_container; pub struct AppModel { client_context: Arc>, @@ -64,6 +69,7 @@ pub enum AppMsg { IcedEvent(Event), WidgetEvent { plugin_id: PluginId, + render_location: RenderLocation, widget_event: ComponentWidgetEvent, }, Noop, @@ -99,9 +105,7 @@ impl Application for AppModel { fn new(_flags: Self::Flags) -> (Self, Command) { let (context_tx, request_rx) = channel::<(PluginId, NativeUiRequestData), NativeUiResponseData>(); - let client_context = Arc::new(StdRwLock::new( - ClientContext { containers: Default::default() } - )); + let client_context = Arc::new(StdRwLock::new(ClientContext::new())); let (dbus_connection, dbus_server, dbus_client) = futures::executor::block_on(async { let path = "/dev/projectgauntlet/Client"; @@ -158,9 +162,6 @@ impl Application for AppModel { entrypoint_id: entrypoint_id.clone(), }); - let mut client_context = self.client_context.write().expect("lock is poisoned"); - client_context.create_view_container(plugin_id.clone()); - Command::batch([ // TODO re-center the window iced::window::resize(Size::new(SUB_VIEW_WINDOW_WIDTH, SUB_VIEW_WINDOW_HEIGHT)), @@ -183,15 +184,22 @@ impl Application for AppModel { .await .unwrap() .into_iter() - .map(|search_result| NativeUiSearchResult { - plugin_id: PluginId::from_string(search_result.plugin_id), - plugin_name: search_result.plugin_name, - entrypoint_id: EntrypointId::new(search_result.entrypoint_id), - entrypoint_name: search_result.entrypoint_name, - entrypoint_type: match search_result.entrypoint_type { + .flat_map(|search_result| { + let entrypoint_type = match search_result.entrypoint_type { DBusEntrypointType::Command => SearchResultEntrypointType::Command, DBusEntrypointType::View => SearchResultEntrypointType::View, - }, + DBusEntrypointType::InlineView => { + return None + }, + }; + + Some(NativeUiSearchResult { + plugin_id: PluginId::from_string(search_result.plugin_id), + plugin_name: search_result.plugin_name, + entrypoint_id: EntrypointId::new(search_result.entrypoint_id), + entrypoint_name: search_result.entrypoint_name, + entrypoint_type, + }) }) .collect(); @@ -236,7 +244,7 @@ impl Application for AppModel { } AppMsg::IcedEvent(_) => Command::none(), AppMsg::WidgetEvent { widget_event: ComponentWidgetEvent::PreviousView, .. } => self.previous_view(), - AppMsg::WidgetEvent { widget_event, plugin_id } => { + AppMsg::WidgetEvent { widget_event, plugin_id, render_location } => { let dbus_client = self.dbus_client.clone(); let client_context = self.client_context.clone(); @@ -244,7 +252,7 @@ impl Application for AppModel { let signal_context = dbus_client.signal_context(); let future = { let client_context = client_context.read().expect("lock is poisoned"); - client_context.handle_event(signal_context, &plugin_id, widget_event) + client_context.handle_event(signal_context, render_location, &plugin_id, widget_event) }; future.await; @@ -259,8 +267,6 @@ impl Application for AppModel { } fn view(&self) -> Element<'_, Self::Message> { - let client_context = self.client_context.clone(); - match &self.view_data { None => { let input: Element<_> = text_input("Search...", self.prompt.as_ref().unwrap_or(&"".to_owned())) @@ -287,21 +293,41 @@ impl Application for AppModel { .width(Length::Fill) .into(); - let column: Element<_> = column(vec![ - container(input) - .width(Length::Fill) - .padding(Padding::new(10.0)) - .into(), - horizontal_rule(1) - .into(), - container(list) - .width(Length::Fill) - .height(Length::Fill) - .padding(Padding::new(5.0)) - .into(), - ]) + let list = container(list) + .width(Length::Fill) + .height(Length::Fill) + .padding(Padding::new(5.0)) .into(); + let input = container(input) + .width(Length::Fill) + .padding(Padding::new(10.0)) + .into(); + + let separator = horizontal_rule(1) + .into(); + + let inline_view_visible = { + let client_context = self.client_context.read().expect("lock is poisoned"); + !client_context.get_all_inline_view_containers().is_empty() + }; + + let column: Element<_> = if inline_view_visible { + column(vec![ + input, + separator, + inline_view_container(self.client_context.clone()).into(), + horizontal_rule(1).into(), + list, + ]).into() + } else { + column(vec![ + input, + separator, + list, + ]).into() + }; + let element: Element<_> = container(column) .style(ContainerStyle::Background) .height(Length::Fixed(WINDOW_HEIGHT as f32)) @@ -312,14 +338,9 @@ impl Application for AppModel { element } Some(ViewData{ plugin_id, entrypoint_id: _, top_level_view: _ }) => { - let container_element: Element = plugin_container(client_context, plugin_id.clone()) + let container_element: Element<_> = view_container(self.client_context.clone(), plugin_id.to_owned()) .into(); - let container_element = container_element.map(|widget_event| AppMsg::WidgetEvent { - plugin_id: plugin_id.to_owned(), - widget_event, - }); - let element: Element<_> = container(container_element) .style(ContainerStyle::Background) .height(Length::Fixed(SUB_VIEW_WINDOW_HEIGHT as f32)) @@ -426,16 +447,21 @@ async fn request_loop( let mut client_context = client_context.write().expect("lock is poisoned"); match request_data { - NativeUiRequestData::ReplaceContainerChildren { top_level_view, container } => { - client_context.replace_container_children(&plugin_id, container); + NativeUiRequestData::ReplaceView { render_location, top_level_view, container } => { + client_context.replace_view(render_location, container, &plugin_id); app_msg = AppMsg::SetTopLevelView(top_level_view); - responder.respond(NativeUiResponseData::ReplaceContainerChildren) + responder.respond(NativeUiResponseData::Nothing) + } + NativeUiRequestData::ClearInlineView => { + client_context.clear_inline_view(&plugin_id); + + responder.respond(NativeUiResponseData::Nothing) } } } let _ = sender.send(app_msg).await; } -} \ No newline at end of file +} diff --git a/rust/client/src/ui/plugin_container.rs b/rust/client/src/ui/plugin_container.rs deleted file mode 100644 index d3c8ac30..00000000 --- a/rust/client/src/ui/plugin_container.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::collections::HashMap; -use std::future::Future; -use std::sync::{Arc, RwLock}; - -use iced::widget::Component; -use iced::widget::component; -use zbus::SignalContext; - -use common::model::PluginId; - -use crate::model::NativeUiWidget; -use crate::ui::theme::{Element, GauntletRenderer}; -use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent, ComponentWidgetWrapper}; - -pub struct PluginContainer { - client_context: Arc>, - plugin_id: PluginId -} - -pub fn plugin_container(client_context: Arc>, plugin_id: PluginId) -> PluginContainer { - PluginContainer { - client_context, - plugin_id - } -} - -impl Component for PluginContainer { - type State = (); - type Event = ComponentWidgetEvent; - - fn update( - &mut self, - _state: &mut Self::State, - event: Self::Event, - ) -> Option { - Some(event) - } - - fn view(&self, _state: &Self::State) -> Element { - let client_context = self.client_context.read().expect("lock is poisoned"); - let container = client_context.get_view_container(&self.plugin_id); - - container.root_widget - .render_widget(ComponentRenderContext::None) - } -} - -impl<'a> From for Element<'a, ComponentWidgetEvent> { - fn from(container: PluginContainer) -> Self { - component(container) - } -} - - -pub struct PluginViewContainer { - root_widget: ComponentWidgetWrapper, -} - -impl PluginViewContainer { - fn new() -> Self { - Self { - root_widget: ComponentWidgetWrapper::root(0), - } - } - - fn create_component_widget(&mut self, ui_widget: NativeUiWidget) -> ComponentWidgetWrapper { - let children = ui_widget.widget_children - .into_iter() - .map(|ui_widget| self.create_component_widget(ui_widget)) - .collect(); - - ComponentWidgetWrapper::widget(ui_widget.widget_id, &ui_widget.widget_type, ui_widget.widget_properties, children) - .expect("unable to create widget") - } - - fn replace_container_children(&mut self, container: NativeUiWidget) { - tracing::trace!("replace_container_children is called. container: {:?}", container); - - let children = container.widget_children.into_iter() - .map(|child| self.create_component_widget(child)) - .collect::>(); - - self.root_widget.find_child_with_id(container.widget_id) - .unwrap_or_else(|| panic!("widget with id {:?} doesn't exist", container.widget_id)) - .set_children(children) - .expect("unable to set children"); - } - - fn handle_event<'a, 'b>(&'a self, signal_context: &'b SignalContext<'_>, plugin_id: PluginId, event: ComponentWidgetEvent) -> impl Future + 'b + Send { - let widget = self.root_widget - .find_child_with_id(event.widget_id()) - .expect("created event for non existing widget?"); - - event.handle(signal_context, plugin_id, widget) - } -} - -pub struct ClientContext { - pub containers: HashMap, -} - -impl ClientContext { - pub fn create_view_container(&mut self, plugin_id: PluginId) { - self.containers.insert(plugin_id, PluginViewContainer::new()); - } - - pub fn get_view_container(&self, plugin_id: &PluginId) -> &PluginViewContainer { - self.containers.get(plugin_id).unwrap() - } - pub fn get_view_container_mut(&mut self, plugin_id: &PluginId) -> &mut PluginViewContainer { - self.containers.get_mut(plugin_id).unwrap() - } - - pub fn replace_container_children(&mut self, plugin_id: &PluginId, container: NativeUiWidget) { - self.get_view_container_mut(plugin_id).replace_container_children(container) - } - - pub fn handle_event<'a>(&self, signal_context: &'a SignalContext<'_>, plugin_id: &'a PluginId, event: ComponentWidgetEvent) -> impl Future + 'a + Send { - self.get_view_container(plugin_id).handle_event(signal_context, plugin_id.clone(), event) - } -} \ No newline at end of file diff --git a/rust/client/src/ui/view_container.rs b/rust/client/src/ui/view_container.rs new file mode 100644 index 00000000..e502324b --- /dev/null +++ b/rust/client/src/ui/view_container.rs @@ -0,0 +1,53 @@ +use std::sync::{Arc, RwLock}; + +use iced::widget::Component; +use iced::widget::component; + +use common::dbus::RenderLocation; +use common::model::PluginId; + +use crate::ui::AppMsg; +use crate::ui::client_context::ClientContext; +use crate::ui::theme::{Element, GauntletRenderer}; +use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent}; + +pub struct ViewContainer { + client_context: Arc>, + plugin_id: PluginId, +} + +pub fn view_container(client_context: Arc>, plugin_id: PluginId) -> ViewContainer { + ViewContainer { + client_context, + plugin_id + } +} + +impl Component for ViewContainer { + type State = (); + type Event = ComponentWidgetEvent; + + fn update( + &mut self, + _state: &mut Self::State, + event: Self::Event, + ) -> Option { + Some(AppMsg::WidgetEvent { + plugin_id: self.plugin_id.clone(), + render_location: RenderLocation::View, + widget_event: event, + }) + } + + fn view(&self, _state: &Self::State) -> Element { + let client_context = self.client_context.read().expect("lock is poisoned"); + let view_container = client_context.get_view_container(); + view_container.render_widget(ComponentRenderContext::None) + } +} + +impl<'a> From for Element<'a, AppMsg> { + fn from(container: ViewContainer) -> Self { + component(container) + } +} \ No newline at end of file diff --git a/rust/client/src/ui/widget.rs b/rust/client/src/ui/widget.rs index dd5d9caa..0172de69 100644 --- a/rust/client/src/ui/widget.rs +++ b/rust/client/src/ui/widget.rs @@ -681,6 +681,49 @@ impl ComponentWidgetWrapper { column(vec![top_panel, separator, content]) .into() } + ComponentWidget::InlineSeparator => { + vertical_rule(1) + .into() + } + ComponentWidget::Inline { children } => { + let contents: Vec<_> = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::Content { .. }), ComponentRenderContext::None) + .into_iter() + .map(|content_element| { + container(content_element) + .width(Length::FillPortion(3)) + // .padding(Padding::from([5.0, 5.0, 0.0, 5.0])) + .into() + }) + .collect(); + + // let mut separators: Vec<_> = render_children_by_type(children, |widget| matches!(widget, ComponentWidget::InlineSeparator { .. }), ComponentRenderContext::None); + + // let mut left = contents.len(); + + let contents: Vec<_> = contents.into_iter() + .flat_map(|i| { + // if left > 1 { + // left = left - 1; + // if separators.is_empty() { + // let separator = vertical_rule(1).into(); + // vec![i, separator] + // } else { + // let separator = separators.remove(0); + // vec![i, separator] + // } + // } else { + vec![i] + // } + }) + .collect(); + + let content: Element<_> = row(contents) + .into(); + + container(content) + .padding(Padding::new(5.0)) + .into() + } } } diff --git a/rust/client/src/ui/widget_container.rs b/rust/client/src/ui/widget_container.rs new file mode 100644 index 00000000..4d165d37 --- /dev/null +++ b/rust/client/src/ui/widget_container.rs @@ -0,0 +1,53 @@ +use zbus::SignalContext; +use common::model::PluginId; +use std::future::Future; +use crate::model::NativeUiWidget; +use crate::ui::theme::Element; +use crate::ui::widget::{ComponentRenderContext, ComponentWidgetEvent, ComponentWidgetWrapper}; + +pub struct PluginWidgetContainer { + root_widget: ComponentWidgetWrapper, +} + +impl PluginWidgetContainer { + pub fn new() -> Self { + Self { + root_widget: ComponentWidgetWrapper::root(0), + } + } + + pub fn render_widget<'a>(&self, context: ComponentRenderContext) -> Element<'a, ComponentWidgetEvent> { + self.root_widget.render_widget(context) + } + + fn create_component_widget(&mut self, ui_widget: NativeUiWidget) -> ComponentWidgetWrapper { + let children = ui_widget.widget_children + .into_iter() + .map(|ui_widget| self.create_component_widget(ui_widget)) + .collect(); + + ComponentWidgetWrapper::widget(ui_widget.widget_id, &ui_widget.widget_type, ui_widget.widget_properties, children) + .expect("unable to create widget") + } + + pub fn replace_view(&mut self, container: NativeUiWidget) { + tracing::trace!("replace_view is called. container: {:?}", container); + + let children = container.widget_children.into_iter() + .map(|child| self.create_component_widget(child)) + .collect::>(); + + self.root_widget.find_child_with_id(container.widget_id) + .unwrap_or_else(|| panic!("widget with id {:?} doesn't exist", container.widget_id)) + .set_children(children) + .expect("unable to set children"); + } + + pub fn handle_event<'a, 'b>(&'a self, signal_context: &'b SignalContext<'_>, plugin_id: PluginId, event: ComponentWidgetEvent) -> impl Future + 'b + Send { + let widget = self.root_widget + .find_child_with_id(event.widget_id()) + .expect("created event for non existing widget?"); + + event.handle(signal_context, plugin_id, widget) + } +} diff --git a/rust/common/src/dbus.rs b/rust/common/src/dbus.rs index 7ce1e69c..99a19f2b 100644 --- a/rust/common/src/dbus.rs +++ b/rust/common/src/dbus.rs @@ -60,6 +60,7 @@ pub struct DbusEventViewEvent { pub enum DBusEntrypointType { Command, View, + InlineView, } pub type DbusUiWidgetId = u32; @@ -94,3 +95,9 @@ pub fn value_number_to_dbus(value: f64) -> DBusUiPropertyValue { pub fn value_bool_to_dbus(value: bool) -> DBusUiPropertyValue { DBusUiPropertyValue(DBusUiPropertyValueType::Bool, Value::Bool(value.into()).to_owned()) } + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, Type)] +pub enum RenderLocation { + InlineView, + View +} diff --git a/rust/component_model/src/lib.rs b/rust/component_model/src/lib.rs index 33d71804..4b903c2f 100644 --- a/rust/component_model/src/lib.rs +++ b/rust/component_model/src/lib.rs @@ -563,10 +563,28 @@ pub fn create_component_model() -> Vec { ]), ); + let inline_separator_component = component( + "inline_separator", + "InlineSeparator", + [], + children_none(), + ); + + let inline_component = component( + "inline", + "Inline", + [], + children_members([ + member("Left", &content_component), + member("Separator", &inline_separator_component), + member("Right", &content_component), + member("Center", &content_component), + ]), + ); let text_part = text_part(); - let root = root(&[&detail_component, &form_component]); + let root = root(&[&detail_component, &form_component, &inline_component]); // Detail // Detail.Content @@ -608,6 +626,12 @@ pub fn create_component_model() -> Vec { // Form.FilePicker // Form.Description + // Inline + // Inline.Left + // Inline.Separator + // Inline.Right + // Inline.Center + // List // List.Dropdown // List.Dropdown.Item @@ -667,6 +691,9 @@ pub fn create_component_model() -> Vec { separator_component, form_component, + inline_separator_component, + inline_component, + root, ] } \ No newline at end of file diff --git a/rust/management_client/src/dbus.rs b/rust/management_client/src/dbus.rs index f8284dc3..f79d3d05 100644 --- a/rust/management_client/src/dbus.rs +++ b/rust/management_client/src/dbus.rs @@ -3,9 +3,9 @@ use common::dbus::DBusPlugin; #[zbus::dbus_proxy( default_service = "dev.projectgauntlet.Gauntlet", default_path = "/dev/projectgauntlet/Server", - interface = "dev.projectgauntlet.Server.Management", + interface = "dev.projectgauntlet.Server", )] -trait DbusManagementServerProxy { +trait DbusServerProxy { #[dbus_proxy(signal)] fn remote_plugin_download_finished_signal(&self, plugin_id: &str) -> zbus::Result<()>; diff --git a/rust/management_client/src/ui.rs b/rust/management_client/src/ui.rs index 91a8fee4..8c8ba322 100644 --- a/rust/management_client/src/ui.rs +++ b/rust/management_client/src/ui.rs @@ -11,7 +11,7 @@ use zbus::Connection; use common::dbus::DBusEntrypointType; use common::model::{EntrypointId, PluginId}; -use crate::dbus::{DbusManagementServerProxyProxy, RemotePluginDownloadFinishedSignalStream}; +use crate::dbus::{DbusServerProxyProxy, RemotePluginDownloadFinishedSignalStream}; pub fn run() { ManagementAppModel::run(Settings { @@ -28,7 +28,7 @@ pub fn run() { struct ManagementAppModel { dbus_connection: Connection, - dbus_server: DbusManagementServerProxyProxy<'static>, + dbus_server: DbusServerProxyProxy<'static>, columns: Vec, plugins: HashMap, selected_item: SelectedItem, @@ -106,6 +106,7 @@ struct Entrypoint { pub enum EntrypointType { Command, View, + InlineView, } impl Application for ManagementAppModel { @@ -120,9 +121,9 @@ impl Application for ManagementAppModel { .build() .await?; - let dbus_server = DbusManagementServerProxyProxy::new(&dbus_connection).await?; + let dbus_server = DbusServerProxyProxy::new(&dbus_connection).await?; - Ok::<(Connection, DbusManagementServerProxyProxy<'_>), anyhow::Error>((dbus_connection, dbus_server)) + Ok::<(Connection, DbusServerProxyProxy<'_>), anyhow::Error>((dbus_connection, dbus_server)) }).unwrap(); let dbus_server_clone = dbus_server.clone(); @@ -554,7 +555,8 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { Row::Entrypoint { entrypoint, .. } => { let entrypoint_type = match entrypoint.entrypoint_type { EntrypointType::Command => "Command", - EntrypointType::View => "View" + EntrypointType::View => "View", + EntrypointType::InlineView => "Inline View" }; container(text(entrypoint_type)) @@ -637,7 +639,7 @@ impl<'a, 'b> table::Column<'a, 'b, ManagementAppMsg, Renderer> for Column { } -async fn reload_plugins(dbus_server: DbusManagementServerProxyProxy<'static>) -> HashMap { +async fn reload_plugins(dbus_server: DbusServerProxyProxy<'static>) -> HashMap { let plugins = dbus_server.plugins().await.unwrap(); plugins.into_iter() @@ -652,7 +654,8 @@ async fn reload_plugins(dbus_server: DbusManagementServerProxyProxy<'static>) -> entrypoint_name: entrypoint.entrypoint_name.clone(), entrypoint_type: match entrypoint.entrypoint_type { DBusEntrypointType::Command => EntrypointType::Command, - DBusEntrypointType::View => EntrypointType::View + DBusEntrypointType::View => EntrypointType::View, + DBusEntrypointType::InlineView => EntrypointType::InlineView } }; (id, entrypoint) diff --git a/rust/server/src/dbus.rs b/rust/server/src/dbus.rs index 76333038..96aa3a46 100644 --- a/rust/server/src/dbus.rs +++ b/rust/server/src/dbus.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use zbus::DBusError; -use common::dbus::{DBusEntrypointType, DbusEventRenderView, DbusEventRunCommand, DbusEventViewEvent, DBusPlugin, DBusSearchResult, DBusUiWidget}; +use common::dbus::{DBusEntrypointType, DbusEventRenderView, DbusEventRunCommand, DbusEventViewEvent, DBusPlugin, DBusSearchResult, DBusUiWidget, RenderLocation}; use common::model::{EntrypointId, PluginId}; use crate::model::PluginEntrypointType; @@ -11,6 +11,7 @@ use crate::search::SearchIndex; pub struct DbusServer { pub search_index: SearchIndex, + pub application_manager: ApplicationManager, } #[zbus::dbus_interface(name = "dev.projectgauntlet.Server")] @@ -19,31 +20,30 @@ impl DbusServer { let result = self.search_index.create_handle() .search(text)? .into_iter() - .map(|item| { - DBusSearchResult { - entrypoint_type: match item.entrypoint_type { - PluginEntrypointType::Command => DBusEntrypointType::Command, - PluginEntrypointType::View => DBusEntrypointType::View, - }, + .flat_map(|item| { + let entrypoint_type = match item.entrypoint_type { + PluginEntrypointType::Command => DBusEntrypointType::Command, + PluginEntrypointType::View => DBusEntrypointType::View, + PluginEntrypointType::InlineView => { + return None + } + }; + + Some(DBusSearchResult { + entrypoint_type, entrypoint_name: item.entrypoint_name, entrypoint_id: item.entrypoint_id, plugin_name: item.plugin_name, plugin_id: item.plugin_id, - } + }) }) .collect(); + self.application_manager.handle_inline_view(text); + Ok(result) } -} - -pub struct DbusManagementServer { - pub application_manager: ApplicationManager, -} - -#[zbus::dbus_interface(name = "dev.projectgauntlet.Server.Management")] -impl DbusManagementServer { #[dbus_interface(signal)] pub async fn remote_plugin_download_finished_signal(signal_ctxt: &zbus::SignalContext<'_>, plugin_id: &str) -> zbus::Result<()>; @@ -140,5 +140,7 @@ trait DbusClientProxy { #[dbus_proxy(signal)] fn view_event_signal(&self, plugin_id: &str, event: DbusEventViewEvent) -> zbus::Result<()>; - fn replace_container_children(&self, plugin_id: &str, top_level_view: bool, container: DBusUiWidget) -> zbus::Result<()>; + fn replace_view(&self, plugin_id: &str, render_location: RenderLocation, top_level_view: bool, container: DBusUiWidget) -> zbus::Result<()>; + + fn clear_inline_view(&self, plugin_id: &str) -> zbus::Result<()>; } diff --git a/rust/server/src/lib.rs b/rust/server/src/lib.rs index 0dcc76f7..b7d17af8 100644 --- a/rust/server/src/lib.rs +++ b/rust/server/src/lib.rs @@ -1,4 +1,4 @@ -use crate::dbus::{DbusManagementServer, DbusServer}; +use crate::dbus::DbusServer; use crate::plugins::ApplicationManager; use crate::search::SearchIndex; @@ -38,13 +38,11 @@ async fn run_server() -> anyhow::Result<()> { application_manager.reload_all_plugins().await?; // TODO do not return here ? - let interface = DbusServer { search_index }; - let management_interface = DbusManagementServer { application_manager }; + let interface = DbusServer { search_index, application_manager }; let _conn = zbus::ConnectionBuilder::session()? .name("dev.projectgauntlet.Gauntlet")? .serve_at("/dev/projectgauntlet/Server", interface)? - .serve_at("/dev/projectgauntlet/Server", management_interface)? .build() .await?; diff --git a/rust/server/src/model.rs b/rust/server/src/model.rs index 0ca8b88f..224bcc23 100644 --- a/rust/server/src/model.rs +++ b/rust/server/src/model.rs @@ -4,7 +4,7 @@ use deno_core::serde_v8; use serde::{Deserialize, Serialize}; use zbus::zvariant::Value; -use common::dbus::{DBusUiPropertyContainer, DBusUiPropertyValue, DBusUiPropertyValueType, DBusUiWidget, value_bool_to_dbus, value_number_to_dbus, value_string_to_dbus}; +use common::dbus::{DBusUiPropertyContainer, DBusUiPropertyValue, DBusUiPropertyValueType, DBusUiWidget, RenderLocation, value_bool_to_dbus, value_number_to_dbus, value_string_to_dbus}; #[derive(Debug)] pub enum JsUiResponseData { @@ -13,10 +13,12 @@ pub enum JsUiResponseData { #[derive(Debug)] pub enum JsUiRequestData { - ReplaceContainerChildren { + ReplaceView { + render_location: RenderLocation, top_level_view: bool, container: IntermediateUiWidget, }, + ClearInlineView } pub type UiWidgetId = u32; @@ -45,6 +47,10 @@ pub enum JsUiEvent { PluginCommand { #[serde(rename = "commandType")] command_type: String, + }, + OpenInlineView { + #[serde(rename = "text")] + text: String, } } @@ -92,6 +98,9 @@ pub enum IntermediateUiEvent { }, PluginCommand { command_type: String, + }, + OpenInlineView { + text: String, } } @@ -171,12 +180,14 @@ fn from_intermediate_to_dbus_properties(value: HashMap &'static str { match value { PluginEntrypointType::Command => "command", PluginEntrypointType::View => "view", + PluginEntrypointType::InlineView => "inline-view", } } @@ -184,6 +195,7 @@ pub fn entrypoint_from_str(value: &str) -> PluginEntrypointType { match value { "command" => PluginEntrypointType::Command, "view" => PluginEntrypointType::View, + "inline-view" => PluginEntrypointType::InlineView, _ => { panic!("index contains illegal entrypoint_type: {}", value) } diff --git a/rust/server/src/plugins/data_db_repository.rs b/rust/server/src/plugins/data_db_repository.rs index 217ae751..fa0dc9a7 100644 --- a/rust/server/src/plugins/data_db_repository.rs +++ b/rust/server/src/plugins/data_db_repository.rs @@ -162,6 +162,17 @@ impl DataDbRepository { Ok(result) } + pub async fn get_inline_view_entrypoint_id_for_plugin(&self, plugin_id: &str) -> anyhow::Result> { + // language=SQLite + let entrypoint_id = sqlx::query_as::<_, (String, )>("SELECT id FROM plugin_entrypoint WHERE plugin_id = ?1 AND type = 'inline-view'") + .bind(plugin_id) + .fetch_optional(&self.pool) + .await? + .map(|result| result.0); + + Ok(entrypoint_id) + } + pub async fn list_pending_plugins(&self) -> anyhow::Result> { // language=SQLite let plugins = sqlx::query_as::<_, GetPendingPlugin>("SELECT * FROM pending_plugin") diff --git a/rust/server/src/plugins/js.rs b/rust/server/src/plugins/js.rs index bedb1e2d..14253f9b 100644 --- a/rust/server/src/plugins/js.rs +++ b/rust/server/src/plugins/js.rs @@ -15,6 +15,7 @@ use deno_runtime::worker::WorkerOptions; use futures_concurrency::stream::Merge; use once_cell::sync::Lazy; use regex::Regex; +use common::dbus::RenderLocation; use common::model::PluginId; use component_model::{Children, Component, create_component_model, PropertyType}; @@ -26,6 +27,7 @@ use crate::plugins::run_status::RunStatusGuard; pub struct PluginRuntimeData { pub id: PluginId, pub code: PluginCode, + pub inline_view_entrypoint_id: Option, pub permissions: PluginPermissions, pub command_receiver: tokio::sync::broadcast::Receiver, } @@ -46,16 +48,28 @@ pub struct PluginPermissions { } #[derive(Clone, Debug)] -pub struct PluginCommand { - pub id: PluginId, - pub data: PluginCommandData, +pub enum PluginCommand { + One { + id: PluginId, + data: OnePluginCommandData, + }, + All { + data: AllPluginCommandData, + } } #[derive(Clone, Debug)] -pub enum PluginCommandData { +pub enum OnePluginCommandData { Stop } +#[derive(Clone, Debug)] +pub enum AllPluginCommandData { + OpenInlineView { + text: String + } +} + pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: RunStatusGuard) -> anyhow::Result<()> { let conn = zbus::Connection::session().await?; let client_proxy = DbusClientProxyProxy::new(&conn).await?; @@ -141,16 +155,25 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run .filter_map(move |command: PluginCommand| { let plugin_id = plugin_id.clone(); async move { - let id = command.id; - - if id != plugin_id { - None - } else { - match command.data { - PluginCommandData::Stop => { - Some(IntermediateUiEvent::PluginCommand { - command_type: "stop".to_string(), - }) + match command { + PluginCommand::One { id, data } => { + if id != plugin_id { + None + } else { + match data { + OnePluginCommandData::Stop => { + Some(IntermediateUiEvent::PluginCommand { + command_type: "stop".to_string(), + }) + } + } + } + } + PluginCommand::All { data } => { + match data { + AllPluginCommandData::OpenInlineView { text } => { + Some(IntermediateUiEvent::OpenInlineView { text }) + } } } } @@ -169,7 +192,15 @@ pub async fn start_plugin_runtime(data: PluginRuntimeData, run_status_guard: Run .build() .expect("unable to start tokio runtime for plugin") .block_on(tokio::task::unconstrained(async move { - start_js_runtime(data.id, data.code, data.permissions, event_stream, client_proxy, component_model).await + start_js_runtime( + data.id, + data.code, + data.permissions, + data.inline_view_entrypoint_id, + event_stream, + client_proxy, + component_model + ).await })); if let Err(err) = result { @@ -191,6 +222,7 @@ async fn start_js_runtime( plugin_id: PluginId, code: PluginCode, permissions: PluginPermissions, + inline_view_entrypoint_id: Option, event_stream: Pin>>, client_proxy: DbusClientProxyProxy<'static>, component_model: Vec, @@ -232,7 +264,7 @@ async fn start_js_runtime( module_loader: Rc::new(CustomModuleLoader::new(code)), extensions: vec![plugin_ext::init_ops_and_esm( EventReceiver::new(event_stream), - PluginData::new(plugin_id), + PluginData::new(plugin_id, inline_view_entrypoint_id), DbusClient::new(client_proxy), ComponentModel::new(component_model), )], @@ -357,7 +389,9 @@ deno_core::extension!( op_log_error, op_component_model, op_plugin_get_pending_event, - op_react_replace_container_children, + op_react_replace_view, + op_inline_view_endpoint_id, + clear_inline_view, ], options = { event_receiver: EventReceiver, @@ -425,28 +459,51 @@ async fn op_plugin_get_pending_event(state: Rc>) -> anyhow::Res Ok(from_intermediate_to_js_event(event)) } +#[op] +fn clear_inline_view(state: Rc>) -> anyhow::Result<()> { + let data = JsUiRequestData::ClearInlineView; + + match make_request(&state, data).context("ClearInlineView frontend response")? { + JsUiResponseData::Nothing => { + tracing::trace!(target = "renderer_rs_persistence", "Calling clear_inline_view returned"); + Ok(()) + } + value @ _ => panic!("unsupported response type {:?}", value), + } +} + +#[op] +fn op_inline_view_endpoint_id(state: Rc>) -> Option { + state.borrow() + .borrow::() + .inline_view_entrypoint_id() + .clone() +} + #[op(v8)] -fn op_react_replace_container_children( +fn op_react_replace_view( scope: &mut v8::HandleScope, state: Rc>, + render_location: RenderLocation, top_level_view: bool, container: JsUiWidget, ) -> anyhow::Result<()> { - tracing::trace!(target = "renderer_rs_persistence", "Calling op_react_replace_container_children..."); + tracing::trace!(target = "renderer_rs_persistence", "Calling op_react_replace_view..."); // TODO fix validation // for new_child in &container.widget_children { // validate_child(&state, &container.widget_type, &new_child.widget_type)? // } - let data = JsUiRequestData::ReplaceContainerChildren { + let data = JsUiRequestData::ReplaceView { + render_location, top_level_view, container: from_js_to_intermediate_widget(scope, container)?, }; - match make_request(&state, data).context("ReplaceContainerChildren frontend response")? { + match make_request(&state, data).context("ReplaceView frontend response")? { JsUiResponseData::Nothing => { - tracing::trace!(target = "renderer_rs_persistence", "Calling op_react_replace_container_children returned"); + tracing::trace!(target = "renderer_rs_persistence", "Calling op_react_replace_view returned"); Ok(()) } value @ _ => panic!("unsupported response type {:?}", value), @@ -602,14 +659,23 @@ fn validate_child(state: &Rc>, parent_internal_name: &str, chil async fn make_request_async(plugin_id: PluginId, dbus_client: DbusClientProxyProxy<'_>, data: JsUiRequestData) -> anyhow::Result { match data { - JsUiRequestData::ReplaceContainerChildren { top_level_view, container } => { - let nothing = dbus_client.replace_container_children(&plugin_id.to_string(), top_level_view, container.into()) + JsUiRequestData::ReplaceView { render_location, top_level_view, container } => { + let nothing = dbus_client.replace_view(&plugin_id.to_string(), render_location, top_level_view, container.into()) .await .map(|_| JsUiResponseData::Nothing) .map_err(|err| err.into()); nothing } + JsUiRequestData::ClearInlineView => { + let nothing = dbus_client.clear_inline_view(&plugin_id.to_string()) + .await + .map(|_| JsUiResponseData::Nothing) + .map_err(|err| err.into()); + + nothing + + } } } @@ -640,7 +706,8 @@ fn from_intermediate_to_js_event(event: IntermediateUiEvent) -> JsUiEvent { } IntermediateUiEvent::PluginCommand { command_type } => JsUiEvent::PluginCommand { command_type - } + }, + IntermediateUiEvent::OpenInlineView { text } => JsUiEvent::OpenInlineView { text } } } @@ -683,16 +750,24 @@ fn from_js_to_intermediate_properties( pub struct PluginData { plugin_id: PluginId, + inline_view_entrypoint_id: Option } impl PluginData { - fn new(plugin_id: PluginId) -> Self { - Self { plugin_id } + fn new(plugin_id: PluginId, inline_view_entrypoint_id: Option) -> Self { + Self { + plugin_id, + inline_view_entrypoint_id + } } fn plugin_id(&self) -> PluginId { self.plugin_id.clone() } + + fn inline_view_entrypoint_id(&self) -> Option { + self.inline_view_entrypoint_id.clone() + } } pub struct DbusClient { diff --git a/rust/server/src/plugins/loader.rs b/rust/server/src/plugins/loader.rs index c523ed92..be2996e2 100644 --- a/rust/server/src/plugins/loader.rs +++ b/rust/server/src/plugins/loader.rs @@ -8,8 +8,8 @@ use anyhow::{anyhow, Context}; use serde::Deserialize; use common::model::PluginId; +use crate::dbus::DbusServer; -use crate::dbus::DbusManagementServer; use crate::model::{entrypoint_to_str, PluginEntrypointType}; use crate::plugins::data_db_repository::{Code, DataDbRepository, PluginPermissions, SavePlugin, SavePluginEntrypoint}; @@ -42,7 +42,7 @@ impl PluginLoader { let plugin_data = PluginLoader::read_plugin_dir(temp_dir.path(), plugin_id.clone()) .await?; - DbusManagementServer::remote_plugin_download_finished_signal(&signal_context, &plugin_id.to_string()) + DbusServer::remote_plugin_download_finished_signal(&signal_context, &plugin_id.to_string()) .await?; data_db_repository.save_plugin(SavePlugin { @@ -155,6 +155,7 @@ impl PluginLoader { entrypoint_type: entrypoint_to_str(match entrypoint.entrypoint_type { PluginConfigEntrypointTypes::Command => PluginEntrypointType::Command, PluginConfigEntrypointTypes::View => PluginEntrypointType::View, + PluginConfigEntrypointTypes::InlineView => PluginEntrypointType::InlineView }).to_owned() }) .collect(); @@ -216,6 +217,8 @@ pub enum PluginConfigEntrypointTypes { Command, #[serde(rename = "view")] View, + #[serde(rename = "inline-view")] + InlineView, } #[derive(Debug, Deserialize)] diff --git a/rust/server/src/plugins/mod.rs b/rust/server/src/plugins/mod.rs index 03e6e70a..44be2a1b 100644 --- a/rust/server/src/plugins/mod.rs +++ b/rust/server/src/plugins/mod.rs @@ -5,7 +5,7 @@ use crate::dirs::Dirs; use crate::model::{entrypoint_from_str, PluginEntrypointType}; use crate::plugins::config_reader::ConfigReader; use crate::plugins::data_db_repository::DataDbRepository; -use crate::plugins::js::{PluginCode, PluginCommand, PluginCommandData, PluginPermissions, PluginRuntimeData, start_plugin_runtime}; +use crate::plugins::js::{PluginCode, PluginCommand, OnePluginCommandData, PluginPermissions, PluginRuntimeData, start_plugin_runtime, AllPluginCommandData}; use crate::plugins::loader::PluginLoader; use crate::plugins::run_status::RunStatusHolder; use crate::search::{SearchIndex, SearchItem}; @@ -81,6 +81,7 @@ impl ApplicationManager { entrypoint_type: match entrypoint_from_str(&entrypoint.entrypoint_type) { PluginEntrypointType::Command => DBusEntrypointType::Command, PluginEntrypointType::View => DBusEntrypointType::View, + PluginEntrypointType::InlineView => DBusEntrypointType::InlineView } }) .collect(); @@ -162,6 +163,14 @@ impl ApplicationManager { Ok(()) } + pub fn handle_inline_view(&self, text: &str) { + self.send_command(PluginCommand::All { + data: AllPluginCommandData::OpenInlineView { + text: text.to_owned() + } + }) + } + async fn reload_plugin(&mut self, plugin_id: PluginId) -> anyhow::Result<()> { let running = self.run_status_holder.is_plugin_running(&plugin_id); if running { @@ -183,13 +192,19 @@ impl ApplicationManager { async fn start_plugin(&mut self, plugin_id: PluginId) -> anyhow::Result<()> { tracing::info!(target = "plugin", "Starting plugin with id: {:?}", plugin_id); - let plugin = self.db_repository.get_plugin_by_id(&plugin_id.to_string()) + let plugin_id_str = plugin_id.to_string(); + + let plugin = self.db_repository.get_plugin_by_id(&plugin_id_str) + .await?; + + let inline_view_entrypoint_id = self.db_repository.get_inline_view_entrypoint_id_for_plugin(&plugin_id_str) .await?; let receiver = self.command_broadcaster.subscribe(); let data = PluginRuntimeData { id: plugin_id, code: PluginCode { js: plugin.code.js }, + inline_view_entrypoint_id, permissions: PluginPermissions { environment: plugin.permissions.environment, high_resolution_time: plugin.permissions.high_resolution_time, @@ -211,9 +226,9 @@ impl ApplicationManager { async fn stop_plugin(&mut self, plugin_id: PluginId) { tracing::info!(target = "plugin", "Stopping plugin with id: {:?}", plugin_id); - let data = PluginCommand { + let data = PluginCommand::One { id: plugin_id, - data: PluginCommandData::Stop, + data: OnePluginCommandData::Stop, }; self.send_command(data)