Compare commits

...

2 Commits

Author SHA1 Message Date
Nate Butler
152d69f273 wip 2025-04-28 22:21:15 -04:00
Nate Butler
9b9746a859 wip 2025-04-28 21:51:31 -04:00
11 changed files with 378 additions and 17 deletions

11
Cargo.lock generated
View File

@@ -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"

View File

@@ -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" }

View File

@@ -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(),

View 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!

View File

@@ -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))
// },
// )
// }
// }

View File

@@ -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...
}

View 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

View File

@@ -0,0 +1 @@
/Users/natebutler/code/zed/LICENSE-GPL

View 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);
}
}

View File

@@ -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(

View File

@@ -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!"