pinakes-ui: add WidgetContainer; basic widget injection system
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I7ca9a47e9b085b8c49869586c90034816a6a6964
This commit is contained in:
parent
62058a7c4d
commit
1acff0227c
2 changed files with 121 additions and 1 deletions
|
|
@ -33,6 +33,8 @@ pub mod actions;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
pub mod widget;
|
||||||
|
|
||||||
pub use registry::{PluginPage, PluginRegistry};
|
pub use registry::PluginRegistry;
|
||||||
pub use renderer::PluginViewRenderer;
|
pub use renderer::PluginViewRenderer;
|
||||||
|
pub use widget::{WidgetContainer, WidgetLocation};
|
||||||
|
|
|
||||||
118
crates/pinakes-ui/src/plugin_ui/widget.rs
Normal file
118
crates/pinakes-ui/src/plugin_ui/widget.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
//! Widget injection system for plugin UI
|
||||||
|
//!
|
||||||
|
//! Allows plugins to inject small UI elements into existing host pages at
|
||||||
|
//! predefined locations. Unlike full pages, widgets have no data sources of
|
||||||
|
//! their own and render with empty data context.
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use pinakes_plugin_api::UiWidget;
|
||||||
|
|
||||||
|
use super::{data::PluginPageData, renderer::render_element};
|
||||||
|
use crate::client::ApiClient;
|
||||||
|
|
||||||
|
/// Predefined injection points in the host UI.
|
||||||
|
///
|
||||||
|
/// These correspond to the string constants in
|
||||||
|
/// `pinakes_plugin_api::widget_location` and determine where a widget is
|
||||||
|
/// rendered.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum WidgetLocation {
|
||||||
|
LibraryHeader,
|
||||||
|
LibrarySidebar,
|
||||||
|
DetailPanel,
|
||||||
|
SearchFilters,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetLocation {
|
||||||
|
/// Returns the canonical string identifier used in plugin manifests.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::LibraryHeader => "library_header",
|
||||||
|
Self::LibrarySidebar => "library_sidebar",
|
||||||
|
Self::DetailPanel => "detail_panel",
|
||||||
|
Self::SearchFilters => "search_filters",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Props for [`WidgetContainer`].
|
||||||
|
#[derive(Props, PartialEq, Clone)]
|
||||||
|
pub struct WidgetContainerProps {
|
||||||
|
/// Injection point to render widgets for.
|
||||||
|
pub location: WidgetLocation,
|
||||||
|
/// All widgets from all plugins (plugin_id, widget) pairs.
|
||||||
|
pub widgets: Vec<(String, UiWidget)>,
|
||||||
|
/// API client (unused by widgets themselves but threaded through for
|
||||||
|
/// consistency with the rest of the plugin UI system).
|
||||||
|
pub client: Signal<ApiClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders all widgets registered for a specific [`WidgetLocation`].
|
||||||
|
///
|
||||||
|
/// Returns `None` if no widgets target this location.
|
||||||
|
///
|
||||||
|
/// # Usage
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// // In a host component:
|
||||||
|
/// WidgetContainer {
|
||||||
|
/// location: WidgetLocation::LibraryHeader,
|
||||||
|
/// widgets: plugin_registry.read().all_widgets(),
|
||||||
|
/// client,
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
pub fn WidgetContainer(props: WidgetContainerProps) -> Element {
|
||||||
|
let location_str = props.location.as_str();
|
||||||
|
let matching: Vec<_> = props
|
||||||
|
.widgets
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, w)| w.target == location_str)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if matching.is_empty() {
|
||||||
|
return rsx! {};
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { class: "plugin-widget-container", "data-location": location_str,
|
||||||
|
for (plugin_id , widget) in &matching {
|
||||||
|
WidgetViewRenderer {
|
||||||
|
plugin_id: plugin_id.clone(),
|
||||||
|
widget: widget.clone(),
|
||||||
|
client: props.client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Props for [`WidgetViewRenderer`].
|
||||||
|
#[derive(Props, PartialEq, Clone)]
|
||||||
|
pub struct WidgetViewRendererProps {
|
||||||
|
/// Plugin that owns this widget.
|
||||||
|
pub plugin_id: String,
|
||||||
|
/// Widget definition to render.
|
||||||
|
pub widget: UiWidget,
|
||||||
|
/// API client signal.
|
||||||
|
pub client: Signal<ApiClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a single plugin widget with an empty data context.
|
||||||
|
///
|
||||||
|
/// Widgets do not declare data sources; they render statically (or use
|
||||||
|
/// inline expressions with no external data).
|
||||||
|
#[component]
|
||||||
|
pub fn WidgetViewRenderer(props: WidgetViewRendererProps) -> Element {
|
||||||
|
let empty_data = PluginPageData::default();
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "plugin-widget",
|
||||||
|
"data-plugin-id": props.plugin_id.clone(),
|
||||||
|
"data-widget-id": props.widget.id.clone(),
|
||||||
|
{ render_element(&props.widget.content, &empty_data, props.client) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue