Compare commits
2 Commits
devcontain
...
stateful-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
152d69f273 | ||
|
|
9b9746a859 |
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -3219,6 +3219,17 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "component_state"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"component",
|
||||
"gpui",
|
||||
"linkme",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
|
||||
@@ -33,6 +33,7 @@ members = [
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/component_state",
|
||||
"crates/context_server",
|
||||
"crates/context_server_settings",
|
||||
"crates/copilot",
|
||||
@@ -242,6 +243,7 @@ command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
component_state = { path = "crates/component_state" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
context_server_settings = { path = "crates/context_server_settings" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
|
||||
@@ -37,9 +37,14 @@ pub trait Component {
|
||||
fn description() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
fn __has_state() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[distributed_slice]
|
||||
@@ -49,7 +54,7 @@ pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
|
||||
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
|
||||
|
||||
pub struct ComponentRegistry {
|
||||
components: Vec<(
|
||||
components: HashMap<ComponentId, (
|
||||
ComponentScope,
|
||||
// name
|
||||
&'static str,
|
||||
@@ -64,7 +69,7 @@ pub struct ComponentRegistry {
|
||||
impl ComponentRegistry {
|
||||
fn new() -> Self {
|
||||
ComponentRegistry {
|
||||
components: Vec::new(),
|
||||
components: HashMap::default(),
|
||||
previews: HashMap::default(),
|
||||
}
|
||||
}
|
||||
@@ -80,7 +85,7 @@ pub fn init() {
|
||||
pub fn register_component<T: Component>() {
|
||||
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
|
||||
let mut data = COMPONENT_DATA.write();
|
||||
data.components.push(component_data);
|
||||
data.components.insert(ComponentId(T::name()), component_data);
|
||||
data.previews.insert(T::name(), T::preview);
|
||||
}
|
||||
|
||||
@@ -153,6 +158,11 @@ impl AllComponents {
|
||||
components.sort_by_key(|a| a.name());
|
||||
components
|
||||
}
|
||||
|
||||
/// Tries to get a [`ComponentId`] by its name.
|
||||
pub fn id_by_name(&self, name: &str) -> Option<ComponentId> {
|
||||
self.0.values().find(|c| c.name() == name).map(|c| c.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AllComponents {
|
||||
@@ -171,15 +181,14 @@ impl DerefMut for AllComponents {
|
||||
pub fn components() -> AllComponents {
|
||||
let data = COMPONENT_DATA.read();
|
||||
let mut all_components = AllComponents::new();
|
||||
for (scope, name, sort_name, description) in &data.components {
|
||||
for (id, (scope, name, sort_name, description)) in &data.components {
|
||||
let preview = data.previews.get(name).cloned();
|
||||
let component_name = SharedString::new_static(name);
|
||||
let sort_name = SharedString::new_static(sort_name);
|
||||
let id = ComponentId(name);
|
||||
all_components.insert(
|
||||
id.clone(),
|
||||
ComponentMetadata {
|
||||
id,
|
||||
id: id.clone(),
|
||||
name: component_name,
|
||||
sort_name,
|
||||
scope: scope.clone(),
|
||||
|
||||
77
crates/component/src/examples/stateful_checkbox.rs
Normal file
77
crates/component/src/examples/stateful_checkbox.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Example usage of the StatefulComponent trait for a checkbox component
|
||||
|
||||
use component::{Component, ComponentScope};
|
||||
use component_preview::stateful_component::StatefulComponent;
|
||||
use gpui::{AnyElement, App, Entity, IntoElement, ToggleButton, Window, div, prelude::*};
|
||||
|
||||
/// Define the component as usual
|
||||
pub struct Checkbox;
|
||||
|
||||
impl Component for Checkbox {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Input
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"ui::Checkbox"
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A checkbox component for toggling boolean values")
|
||||
}
|
||||
|
||||
// Instead of implementing preview directly, we'll implement StatefulComponent
|
||||
// which will automatically provide the preview implementation
|
||||
}
|
||||
|
||||
/// Define the data needed for our preview
|
||||
pub struct CheckboxPreviewData {
|
||||
pub checked: bool,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Implement the StatefulComponent trait to provide stateful previews
|
||||
impl StatefulComponent for Checkbox {
|
||||
type PreviewData = CheckboxPreviewData;
|
||||
|
||||
// Create the initial preview data
|
||||
fn create_preview_data(window: &mut Window, cx: &mut App) -> Entity<Self::PreviewData> {
|
||||
cx.new(|_| CheckboxPreviewData {
|
||||
checked: false,
|
||||
label: "Toggle me".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// Render the component using the preview data
|
||||
fn render_stateful_preview(
|
||||
preview_data: &Entity<Self::PreviewData>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> Option<AnyElement> {
|
||||
// We can safely read our strongly-typed preview data
|
||||
let is_checked = preview_data.read(cx).checked;
|
||||
let label = preview_data.read(cx).label.clone();
|
||||
|
||||
// Create the example component
|
||||
let checkbox = ToggleButton::new("checkbox-example", is_checked)
|
||||
.on_toggle(move |_toggled, window, cx| {
|
||||
// Update the preview data when toggled
|
||||
preview_data.update(cx, |data, cx| {
|
||||
data.checked = !data.checked;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.label(label);
|
||||
|
||||
// Return our example wrapped in a container
|
||||
Some(
|
||||
div()
|
||||
.p_4()
|
||||
.child(checkbox)
|
||||
.into_any_element()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Now when this component is previewed, it will automatically maintain state across renders
|
||||
// No need to do any special thread-local or global state management!
|
||||
@@ -497,15 +497,15 @@ impl ComponentPreview {
|
||||
}
|
||||
|
||||
fn render_preview(
|
||||
&self,
|
||||
&mut self,
|
||||
component: &ComponentMetadata,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let name = component.scopeless_name();
|
||||
let scope = component.scope();
|
||||
|
||||
let description = component.description();
|
||||
// let component_id = component.id();
|
||||
|
||||
v_flex()
|
||||
.py_2()
|
||||
@@ -539,10 +539,7 @@ impl ComponentPreview {
|
||||
.child(description),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(component.preview(), |this, preview| {
|
||||
this.children(preview(window, cx))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -962,3 +959,55 @@ impl RenderOnce for ComponentPreviewPage {
|
||||
.child(self.render_preview(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility to add context params to a Component's preview method
|
||||
pub struct PreviewContext<'a, T: 'static> {
|
||||
/// The preview data entity for the component
|
||||
pub preview_data: Option<Entity<T>>,
|
||||
/// The window for rendering
|
||||
pub window: &'a mut Window,
|
||||
/// The application context
|
||||
pub cx: &'a mut App,
|
||||
}
|
||||
|
||||
// /// Extension utility to pass preview data to a Component's preview method
|
||||
// pub trait ComponentPreviewExt {
|
||||
// /// Pass preview data along with this component preview
|
||||
// fn with_preview_data<T: 'static>(
|
||||
// &mut self,
|
||||
// component_id: &ComponentId,
|
||||
// preview_data: Option<Entity<T>>,
|
||||
// window: &mut Window,
|
||||
// cx: &mut App,
|
||||
// ) -> Option<AnyElement>;
|
||||
// }
|
||||
|
||||
// impl ComponentPreviewExt for ComponentPreview {
|
||||
// fn with_preview_data<T: 'static>(
|
||||
// &mut self,
|
||||
// component_id: &ComponentId,
|
||||
// preview_data: Option<Entity<T>>,
|
||||
// window: &mut Window,
|
||||
// cx: &mut App,
|
||||
// ) -> Option<AnyElement> {
|
||||
// // Store the entity if we have one
|
||||
// if let Some(data) = &preview_data {
|
||||
// let any_entity = data.clone().into_any();
|
||||
// self.preview_data_entities
|
||||
// .insert(component_id.clone(), any_entity);
|
||||
// }
|
||||
|
||||
// // Look up the component metadata
|
||||
// let metadata = self.component_map.get(component_id)?;
|
||||
|
||||
// // Call the appropriate preview method with context
|
||||
// component::Component::preview_with_context(component_id, preview_data, window, cx).or_else(
|
||||
// || {
|
||||
// // Fall back to regular preview
|
||||
// metadata
|
||||
// .preview()
|
||||
// .and_then(|preview_fn| preview_fn(window, cx))
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Example of how ComponentPreview would integrate with StatefulComponent
|
||||
|
||||
use component::{Component, ComponentId};
|
||||
use component_preview::component_preview::ComponentPreview;
|
||||
use component_preview::stateful_component::StatefulComponent;
|
||||
use gpui::{App, Context, Entity, Window};
|
||||
|
||||
// This demonstrates how to switch between regular components and stateful components
|
||||
// in the component preview system
|
||||
|
||||
pub fn render_component_in_preview<C: Component>(
|
||||
component_id: &ComponentId,
|
||||
preview: &mut ComponentPreview,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ComponentPreview>
|
||||
) {
|
||||
// Check if we can use the stateful component approach
|
||||
let is_stateful = can_be_rendered_as_stateful_component::<C>();
|
||||
|
||||
if is_stateful {
|
||||
// For stateful components, we use the specialized rendering path
|
||||
// that maintains state across renders
|
||||
render_stateful_component::<C>(preview, window, cx);
|
||||
} else {
|
||||
// For regular components, use the standard preview function
|
||||
if let Some(preview_fn) = C::preview(window, cx) {
|
||||
// Display the component preview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type-checking helper to see if a Component also implements StatefulComponent
|
||||
fn can_be_rendered_as_stateful_component<C: Component>() -> bool {
|
||||
// This would use a trait bound check in real code
|
||||
// Simplified for this example
|
||||
false
|
||||
}
|
||||
|
||||
// Specialized rendering for StatefulComponent
|
||||
fn render_stateful_component<C: Component + StatefulComponent>(
|
||||
preview: &mut ComponentPreview,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ComponentPreview>
|
||||
) {
|
||||
// This directly calls our type-safe helper
|
||||
let result = preview.render_stateful_component::<C>(window, cx);
|
||||
|
||||
// Use the result for display...
|
||||
}
|
||||
22
crates/component_state/Cargo.toml
Normal file
22
crates/component_state/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "component_state"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/component_state.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
linkme.workspace = true
|
||||
util.workspace = true
|
||||
1
crates/component_state/LICENSE-GPL
Symbolic link
1
crates/component_state/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
/Users/natebutler/code/zed/LICENSE-GPL
|
||||
129
crates/component_state/src/component_state.rs
Normal file
129
crates/component_state/src/component_state.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use component::{Component, ComponentId, components};
|
||||
use gpui::{AnyElement, AnyEntity, App, Entity, Window};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Registry for storing and retrieving stateful component data
|
||||
pub struct StatefulComponentRegistry {
|
||||
/// Component data is stored in `AnyEntity`ies due to cyclic references
|
||||
///
|
||||
/// These can be accessed using the ComponentId as a key, and can then
|
||||
/// be downcasted to the appropriate type in the `component_preview` crate.
|
||||
entities: HashMap<ComponentId, AnyEntity>,
|
||||
|
||||
types: HashMap<ComponentId, TypeId>,
|
||||
}
|
||||
|
||||
impl StatefulComponentRegistry {
|
||||
/// Create a new empty registry
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entities: HashMap::new(),
|
||||
types: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stateful_component_ids(&self) -> Vec<ComponentId> {
|
||||
self.types.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get an entity for a component, or create it if it doesn't exist
|
||||
pub fn get_or_create<T: 'static, F>(
|
||||
&mut self,
|
||||
component_id: &ComponentId,
|
||||
create_fn: F,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<T>
|
||||
where
|
||||
F: FnOnce(&mut Window, &mut App) -> Entity<T>,
|
||||
{
|
||||
if let Some(entity) = self.entities.get(component_id) {
|
||||
if let Ok(typed_entity) = entity.clone().downcast::<T>() {
|
||||
return typed_entity;
|
||||
}
|
||||
|
||||
self.entities.remove(component_id);
|
||||
}
|
||||
|
||||
let entity = create_fn(window, cx);
|
||||
|
||||
self.entities
|
||||
.insert(component_id.clone(), entity.clone().into_any());
|
||||
self.types.insert(component_id.clone(), TypeId::of::<T>());
|
||||
|
||||
entity
|
||||
}
|
||||
|
||||
/// Get an entity if it exists
|
||||
pub fn get<T: 'static>(&self, component_id: &ComponentId) -> Option<Entity<T>> {
|
||||
self.entities
|
||||
.get(component_id)
|
||||
.and_then(|entity| entity.clone().downcast::<T>().ok())
|
||||
}
|
||||
|
||||
/// Check if a component has an entity
|
||||
pub fn has_entity(&self, component_id: &ComponentId) -> bool {
|
||||
self.entities.contains_key(component_id)
|
||||
}
|
||||
|
||||
/// Remove an entity
|
||||
pub fn remove(&mut self, component_id: &ComponentId) -> Option<AnyEntity> {
|
||||
let entity = self.entities.remove(component_id);
|
||||
if entity.is_some() {
|
||||
self.types.remove(component_id);
|
||||
}
|
||||
entity
|
||||
}
|
||||
|
||||
/// Clear all entities
|
||||
pub fn clear(&mut self) {
|
||||
self.entities.clear();
|
||||
self.types.clear();
|
||||
}
|
||||
|
||||
/// Get the number of stored entities
|
||||
pub fn len(&self) -> usize {
|
||||
self.entities.len()
|
||||
}
|
||||
|
||||
/// Check if the registry is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entities.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ComponentState: Component {
|
||||
/// The type of data stored for this component
|
||||
type Data: 'static;
|
||||
|
||||
fn id() -> ComponentId {
|
||||
components()
|
||||
.id_by_name(Self::name())
|
||||
.expect("Couldn't get component ID")
|
||||
}
|
||||
|
||||
/// Create the initial state data for this component
|
||||
fn data(window: &mut Window, cx: &mut App) -> Entity<Self::Data>;
|
||||
|
||||
/// Render this component with its state data
|
||||
fn stateful_preview(
|
||||
data: Entity<Self::Data>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement>;
|
||||
|
||||
fn __has_state() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Internal function to register the component's data with the
|
||||
/// [`StatefulComponentRegistry`].
|
||||
fn __register_data(
|
||||
state_registry: &mut StatefulComponentRegistry,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
state_registry.get_or_create(&Self::id(), Self::data, window, cx);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
|
||||
AnyElement, AnyView, App, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -10,6 +10,9 @@ use crate::{ElevationIndex, KeyBinding, prelude::*};
|
||||
// TODO: Checkbox, CheckboxWithLabel, and Switch could all be
|
||||
// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike
|
||||
|
||||
// Import Component and StatefulComponent
|
||||
use component::{Component, ComponentScope};
|
||||
|
||||
/// Creates a new checkbox.
|
||||
pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
|
||||
Checkbox::new(id, toggle_state)
|
||||
@@ -598,6 +601,11 @@ impl RenderOnce for SwitchWithLabel {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckboxPreviewData {
|
||||
pub checked: ToggleState,
|
||||
pub string: String,
|
||||
}
|
||||
|
||||
impl Component for Checkbox {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Input
|
||||
@@ -608,6 +616,7 @@ impl Component for Checkbox {
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
// The preview is handled by the StatefulComponent implementation
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
@@ -618,6 +627,9 @@ impl Component for Checkbox {
|
||||
single_example(
|
||||
"Unselected",
|
||||
Checkbox::new("checkbox_unselected", ToggleState::Unselected)
|
||||
.on_click(move |toggle_state, _, _| {
|
||||
println!("clicked! toggle_state: {:?}", toggle_state);
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
|
||||
@@ -27,7 +27,7 @@ elif [[ "$LICENSE_FLAG" == *"agpl"* ]]; then
|
||||
LICENSE_FILE="LICENSE-AGPL"
|
||||
else
|
||||
LICENSE_MODE="GPL-3.0-or-later"
|
||||
LICENSE_FILE="LICENSE"
|
||||
LICENSE_FILE="LICENSE-GPL"
|
||||
fi
|
||||
|
||||
if [[ ! "$CRATE_NAME" =~ ^[a-z0-9_]+$ ]]; then
|
||||
@@ -39,7 +39,7 @@ CRATE_PATH="crates/$CRATE_NAME"
|
||||
mkdir -p "$CRATE_PATH/src"
|
||||
|
||||
# Symlink the license
|
||||
ln -sf "../../../$LICENSE_FILE" "$CRATE_PATH/LICENSE"
|
||||
ln -sf "$(pwd)/$LICENSE_FILE" "$CRATE_PATH/$LICENSE_FILE"
|
||||
|
||||
CARGO_TOML_TEMPLATE=$(cat << 'EOF'
|
||||
[package]
|
||||
@@ -82,5 +82,5 @@ echo "$CARGO_TOML_CONTENT" > "$CRATE_PATH/Cargo.toml"
|
||||
echo "//! # $CRATE_NAME" > "$CRATE_PATH/src/$CRATE_NAME.rs"
|
||||
|
||||
echo "Created new crate: $CRATE_NAME in $CRATE_PATH"
|
||||
echo "License: $LICENSE_MODE (symlinked from $LICENSE_FILE)"
|
||||
echo "License: $LICENSE_MODE (symlinked from $LICENSE_FILE to $LICENSE_FILE)"
|
||||
echo "Don't forget to add the new crate to the workspace!"
|
||||
|
||||
Reference in New Issue
Block a user