Skip to content

Commit

Permalink
Implement inline views
Browse files Browse the repository at this point in the history
  • Loading branch information
Exidex committed Feb 13, 2024
1 parent 09c007f commit 461b8d4
Show file tree
Hide file tree
Showing 32 changed files with 750 additions and 273 deletions.
6 changes: 6 additions & 0 deletions dev_plugin/gauntlet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 3 additions & 4 deletions dev_plugin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions dev_plugin/src/inline-view.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Inline>
<Inline.Left>
<Content.Paragraph>
Testing inline view left {props.text}
</Content.Paragraph>
</Inline.Left>
<Inline.Separator/>
<Inline.Right>
<Content.Paragraph>
Testing inline view right
</Content.Paragraph>
</Inline.Right>
</Inline>
)
}
22 changes: 22 additions & 0 deletions js/api/src/gen/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ declare global {
["gauntlet:form"]: {
children?: ElementComponent<typeof TextField | typeof PasswordField | typeof Checkbox | typeof DatePicker | typeof Select | typeof Separator>;
};
["gauntlet:inline_separator"]: {};
["gauntlet:inline"]: {
children?: ElementComponent<typeof Content | typeof InlineSeparator | typeof Content | typeof Content>;
};
}
}
}
Expand Down Expand Up @@ -383,3 +387,21 @@ Form.Checkbox = Checkbox;
Form.DatePicker = DatePicker;
Form.Select = Select;
Form.Separator = Separator;
export const InlineSeparator: FC = (): ReactNode => {
return <gauntlet:inline_separator />;
};
export interface InlineProps {
children?: ElementComponent<typeof Content | typeof InlineSeparator | typeof Content | typeof Content>;
}
export const Inline: FC<InlineProps> & {
Left: typeof Content;
Separator: typeof InlineSeparator;
Right: typeof Content;
Center: typeof Content;
} = (props: InlineProps): ReactNode => {
return <gauntlet:inline children={props.children}/>;
};
Inline.Left = Content;
Inline.Separator = InlineSeparator;
Inline.Right = Content;
Inline.Center = Content;
3 changes: 2 additions & 1 deletion js/core/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { defineConfig } from "rollup";

export default defineConfig({
input: [
'src/init.ts',
'src/init.tsx',
],
output: [
{
Expand All @@ -14,6 +14,7 @@ export default defineConfig({
sourcemap: 'inline',
}
],
external: ["react", "react/jsx-runtime"],
plugins: [
nodeResolve(),
commonjs(),
Expand Down
30 changes: 26 additions & 4 deletions js/core/src/init.ts → js/core/src/init.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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", <View/>);
} catch (e) {
console.error("Error occurred when rendering view", pluginEvent.entrypointId, e)
}
Expand All @@ -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 = <View text={pluginEvent.text}/>;

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": {
Expand Down
6 changes: 3 additions & 3 deletions js/core/typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
2 changes: 1 addition & 1 deletion js/react_renderer/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<View/>)
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())
};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand All @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,6 +21,7 @@ type OpenView = {
frontend: string
entrypointId: string
}

type RunCommand = {
type: "RunCommand"
entrypointId: string
Expand All @@ -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 }
Expand All @@ -55,7 +62,10 @@ interface InternalApi {

op_component_model(): Record<string, Component>;

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
Expand Down
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
22 changes: 17 additions & 5 deletions rust/client/src/dbus.rs
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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<T> = core::result::Result<T, ClientError>;
Expand Down
Loading

0 comments on commit 461b8d4

Please sign in to comment.