Skip to content

Commit

Permalink
feat: better implementation of initial scan and render
Browse files Browse the repository at this point in the history
  • Loading branch information
Charlie-XIAO committed Feb 3, 2025
1 parent 5713891 commit d201840
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 76 deletions.
21 changes: 21 additions & 0 deletions crates/deskulpt-core/src/commands/emit_on_render_ready.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use tauri::{command, AppHandle, Runtime};

use super::error::CmdResult;
use crate::states::StatesExtRenderReady;

/// Emit the `render` event to the canvas when the listener is ready.
///
/// This is a wrapper command for
/// [`emit_on_render_ready`](StatesExtRenderReady::emit_on_render_ready)
/// to be invoked by the frontend.
///
/// ### Errors
///
/// - Failed to emit the `render` event to the canvas.
#[command]
pub async fn emit_on_render_ready<R: Runtime>(
app_handle: AppHandle<R>,
payload: serde_json::Value,
) -> CmdResult<()> {
Ok(app_handle.emit_on_render_ready(payload)?)
}
6 changes: 6 additions & 0 deletions crates/deskulpt-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ mod bundle_widget;
#[doc(hidden)]
mod call_plugin;
#[doc(hidden)]
mod emit_on_render_ready;
#[doc(hidden)]
mod exit_app;
#[doc(hidden)]
mod open_in_widgets_dir;
#[doc(hidden)]
mod rescan_widgets;
#[doc(hidden)]
mod set_render_ready;
#[doc(hidden)]
mod update_shortcut;

mod error;

pub use bundle_widget::*;
pub use call_plugin::*;
pub use emit_on_render_ready::*;
pub use exit_app::*;
pub use open_in_widgets_dir::*;
pub use rescan_widgets::*;
pub use set_render_ready::*;
pub use update_shortcut::*;
18 changes: 18 additions & 0 deletions crates/deskulpt-core/src/commands/set_render_ready.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use tauri::{command, AppHandle, Runtime};

use super::error::CmdResult;
use crate::states::StatesExtRenderReady;

/// Set the `render` listener as ready.
///
/// This is a wrapper command for
/// [`set_render_ready`](StatesExtRenderReady::set_render_ready) to be invoked
/// by the frontend.
///
/// ### Errors
///
/// - Failed to emit the `render` event to the canvas.
#[command]
pub async fn set_render_ready<R: Runtime>(app_handle: AppHandle<R>) -> CmdResult<()> {
Ok(app_handle.set_render_ready()?)
}
5 changes: 5 additions & 0 deletions crates/deskulpt-core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ pub trait EventsExt<R: Runtime>: Emitter<R> {
fn emit_exit_app_to_manager(&self) -> Result<()> {
Ok(self.emit_to("manager", "exit-app", ())?)
}

/// Emit the `render` event to the canvas.
fn emit_render_to_canvas(&self, payload: serde_json::Value) -> Result<()> {
Ok(self.emit_to("canvas", "render", payload)?)
}
}

impl<R: Runtime> EventsExt<R> for AppHandle<R> {}
Expand Down
2 changes: 1 addition & 1 deletion crates/deskulpt-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ pub use events::EventsExt;
pub use path::PathExt;
pub use settings::Settings;
pub use shortcuts::ShortcutsExt;
pub use states::{StatesExtCanvasClickThrough, StatesExtWidgetConfigMap};
pub use states::{StatesExtCanvasClickThrough, StatesExtRenderReady, StatesExtWidgetConfigMap};
pub use tray::TrayExt;
pub use window::{on_window_event, WindowExt};
3 changes: 3 additions & 0 deletions crates/deskulpt-core/src/states/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Deskulpt runtime state management.
mod canvas_click_through;
mod render_ready;
mod widget_config_map;

#[doc(hidden)]
pub use canvas_click_through::StatesExtCanvasClickThrough;
#[doc(hidden)]
pub use render_ready::StatesExtRenderReady;
#[doc(hidden)]
pub use widget_config_map::StatesExtWidgetConfigMap;
62 changes: 62 additions & 0 deletions crates/deskulpt-core/src/states/render_ready.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! State management for whether the `render` listener is ready.
use std::sync::Mutex;

use anyhow::Result;
use tauri::{App, AppHandle, Manager, Runtime};

use crate::EventsExt;

/// Managed state for whether the `render` listener is ready.
///
/// The first parameter indicates whether the listener (on the canvas window)
/// for the `render` event is ready. The second parameter is an optional pending
/// payload for the `render` event. In particular, the manager window will emit
/// a `render` event on startup, at which point the listener may not be ready
/// yet. In this case we need to store the payload and emit it later when the
/// listener is ready.
#[derive(Default)]
struct RenderReadyState(Mutex<(bool, Option<serde_json::Value>)>);

/// Extension trait for operations related to the `render` listener readiness.
pub trait StatesExtRenderReady<R: Runtime>: Manager<R> + EventsExt<R> {
/// Initialize state management for whether the `render` listener is ready.
fn manage_render_ready(&self) {
self.manage(RenderReadyState::default());
}

/// Set the `render` listener as ready.
///
/// If there is a pending payload, emit a `render` event with that payload
/// to the canvas.
fn set_render_ready(&self) -> Result<()> {
let state = self.state::<RenderReadyState>();
let mut render_ready = state.0.lock().unwrap();
render_ready.0 = true;

if let Some(payload) = render_ready.1.take() {
self.emit_render_to_canvas(payload)?;
}
Ok(())
}

/// Emit the `render` event to the canvas when the listener is ready.
///
/// If the `render` listener is not ready, store the given payload as
/// pending so that it can be emitted later when the listener is ready.
/// Otherwise, emit a `render` event with the given payload to the canvas
/// immediately.
fn emit_on_render_ready(&self, payload: serde_json::Value) -> Result<()> {
let state = self.state::<RenderReadyState>();
let mut render_ready = state.0.lock().unwrap();

if !render_ready.0 {
render_ready.1 = Some(payload);
return Ok(());
}
self.emit_render_to_canvas(payload)
}
}

impl<R: Runtime> StatesExtRenderReady<R> for App<R> {}
impl<R: Runtime> StatesExtRenderReady<R> for AppHandle<R> {}
7 changes: 5 additions & 2 deletions crates/deskulpt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
)]

use deskulpt_core::{
PathExt, Settings, ShortcutsExt, StatesExtCanvasClickThrough, StatesExtWidgetConfigMap,
TrayExt, WindowExt,
PathExt, Settings, ShortcutsExt, StatesExtCanvasClickThrough, StatesExtRenderReady,
StatesExtWidgetConfigMap, TrayExt, WindowExt,
};
use tauri::image::Image;
use tauri::{generate_context, generate_handler, include_image, Builder};
Expand All @@ -29,6 +29,7 @@ pub fn run() {
},
};

app.manage_render_ready();
app.manage_widget_config_map();
app.manage_canvas_click_through();

Expand All @@ -49,9 +50,11 @@ pub fn run() {
.invoke_handler(generate_handler![
deskulpt_core::commands::call_plugin,
deskulpt_core::commands::bundle_widget,
deskulpt_core::commands::emit_on_render_ready,
deskulpt_core::commands::exit_app,
deskulpt_core::commands::open_in_widgets_dir,
deskulpt_core::commands::rescan_widgets,
deskulpt_core::commands::set_render_ready,
deskulpt_core::commands::update_shortcut,
])
.plugin(tauri_plugin_clipboard_manager::init())
Expand Down
4 changes: 2 additions & 2 deletions src/canvas/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import WidgetContainer from "./components/WidgetContainer";
import useRenderWidgetListener from "./hooks/useRenderWidgetListener";
import useRenderListener from "./hooks/useRenderListener";
import useRemoveWidgetsListener from "./hooks/useRemoveWidgetsListener";
import useShowToastListener from "./hooks/useShowToastListener";
import { Toaster } from "sonner";
Expand All @@ -15,7 +15,7 @@ const App = () => {
);

useShowToastListener();
useRenderWidgetListener();
useRenderListener();
useRemoveWidgetsListener();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useEffect } from "react";
import { listenToRenderWidget } from "../../events";
import { useCallback, useEffect, useRef } from "react";
import { listenToRender } from "../../events";
import { WidgetSettings } from "../../types/backend";
import { invokeBundleWidget } from "../../commands";
import { invokeBundleWidget, invokeSetRenderReady } from "../../commands";
import {
Widget,
updateWidgetRender,
Expand All @@ -14,9 +14,11 @@ import {
const baseUrl = new URL(import.meta.url).origin;

/**
* Listen and react to the "render-widget" event.
* Listen and react to the "render" event.
*/
export default function useRenderWidgetListener() {
export default function useRenderListener() {
const hasInited = useRef(false);

const bundleWidget = useCallback(
async (id: string, settings: WidgetSettings) => {
// Get the widget APIs blob URL, reusing if applicable
Expand Down Expand Up @@ -83,24 +85,32 @@ export default function useRenderWidgetListener() {
);

useEffect(() => {
const unlisten = listenToRenderWidget((event) => {
const { id, bundle, settings } = event.payload;

// We do not wish to bundle the widget
if (!bundle) {
// Make sure that we do not update settings of a not-yet-rendered widget; note
// that is not an errorneous case because users can update settings in the
// manager without having rendered them on the canvas; the case is, when the
// manager finally requests to bundle, it will carry the latest settings in the
// payload so the canvas can still get the correct information
updateWidgetSettings(id, settings);
return;
}
const unlisten = listenToRender(async (event) => {
const promises = event.payload.map(async ({ id, bundle, settings }) => {
if (bundle) {
await bundleWidget(id, settings);
} else {
// We do not wish to bundle the widget
// Make sure that we do not update settings of a not-yet-rendered widget; note
// that is not an errorneous case because users can update settings in the
// manager without having rendered them on the canvas; the case is, when the
// manager finally requests to bundle, it will carry the latest settings in the
// payload so the canvas can still get the correct information
updateWidgetSettings(id, settings);
}
});

// We do wish to bundle the widget
bundleWidget(id, settings).catch(console.error);
await Promise.all(promises);
});

if (!hasInited.current) {
invokeSetRenderReady()
.then(() => {
hasInited.current = true;
})
.catch(console.error);
}

return () => {
unlisten.then((f) => f()).catch(console.error);
};
Expand Down
15 changes: 15 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { invoke } from "@tauri-apps/api/core";
import { Settings, Shortcuts, WidgetConfig } from "./types/backend";
import { RenderPayload } from "./types/frontend";

/**
* Invoke the `bundle_widget` command.
Expand Down Expand Up @@ -50,3 +51,17 @@ export function invokeUpdateShortcut(payload: {
}) {
return invoke<void>("update_shortcut", payload);
}

/**
* Invoke the `emit_on_render_ready` command.
*/
export function invokeEmitOnRenderReady(payload: { payload: RenderPayload }) {
return invoke<void>("emit_on_render_ready", payload);
}

/**
* Invoke the `set_render_ready` command.
*/
export function invokeSetRenderReady() {
return invoke<void>("set_render_ready");
}
16 changes: 7 additions & 9 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,28 @@
import { EventCallback, emitTo, listen } from "@tauri-apps/api/event";
import {
RemoveWidgetsPayload,
RenderWidgetPayload,
RenderPayload,
UpdateSettingsPayload,
} from "./types/frontend";
import { ShowToastPayload, Theme } from "./types/backend";

/**
* Emit the "render-widget" event to the canvas window.
* Emit the "render" event to the canvas window.
*
* @param payload The payload of the event.
*/
export async function emitRenderWidgetToCanvas(payload: RenderWidgetPayload) {
await emitTo("canvas", "render-widget", payload);
export async function emitRenderToCanvas(payload: RenderPayload) {
await emitTo("canvas", "render", payload);
}

/**
* Listen to the "render-widget" event.
* Listen to the "render" event.
*
* @param handler The callback function to handle the event.
* @returns A promise that resolves to a function to unlisten to the event.
*/
export function listenToRenderWidget(
handler: EventCallback<RenderWidgetPayload>,
) {
return listen("render-widget", handler);
export function listenToRender(handler: EventCallback<RenderPayload>) {
return listen("render", handler);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/manager/components/WidgetContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
WidgetConfigType,
WidgetSettings,
} from "../../types/backend";
import { emitRenderWidgetToCanvas } from "../../events";
import { emitRenderToCanvas } from "../../events";
import { Dispatch, SetStateAction } from "react";
import { ManagerWidgetState } from "../../types/frontend";
import WidgetContentHeading from "../components/WidgetContentHeading";
Expand Down Expand Up @@ -90,7 +90,7 @@ const WidgetContent = ({
actionIcon={<LuRepeat />}
actionText="Re-render"
action={() =>
emitRenderWidgetToCanvas({ id, settings, bundle: true }).then(() =>
emitRenderToCanvas([{ id, settings, bundle: true }]).then(() =>
toast.success(`Re-rendered widget "${id}".`),
)
}
Expand Down
14 changes: 8 additions & 6 deletions src/manager/components/WidgetContentSettingsList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction } from "react";
import { WidgetSettings } from "../../types/backend";
import { ManagerWidgetState } from "../../types/frontend";
import { emitRenderWidgetToCanvas } from "../../events";
import { emitRenderToCanvas } from "../../events";
import { DataList, Flex } from "@radix-ui/themes";
import NumberInput from "../components/NumberInput";
import { FaTimes } from "react-icons/fa";
Expand Down Expand Up @@ -35,11 +35,13 @@ const WidgetContentSettingList = ({
...prev,
[id]: { ...prev[id], settings: newSettings },
}));
emitRenderWidgetToCanvas({
id,
settings: newSettings,
bundle: false,
}).catch(console.error);
emitRenderToCanvas([
{
id,
settings: newSettings,
bundle: false,
},
]).catch(console.error);
}

return (
Expand Down
Loading

0 comments on commit d201840

Please sign in to comment.