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