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 registry;
|
||||
pub mod renderer;
|
||||
pub mod widget;
|
||||
|
||||
pub use registry::{PluginPage, PluginRegistry};
|
||||
pub use registry::PluginRegistry;
|
||||
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