Refine component preview & add serialization (#28545)
https://github.com/user-attachments/assets/0be12a9a-f6ce-4eca-90de-6ef01eb41ff9 - Allows the active ComponentPreview page to be restored via serialization - Allows filtering components using a filter input - Updates component example rendering - Updates some components Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
a03fb3791e
commit
2f5c662c42
7 changed files with 468 additions and 124 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -3175,14 +3175,20 @@ dependencies = [
|
|||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"db",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"languages",
|
||||
"notifications",
|
||||
"project",
|
||||
"serde",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
|
|
@ -191,6 +191,14 @@ pub fn components() -> AllComponents {
|
|||
all_components
|
||||
}
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
// pub enum ComponentStatus {
|
||||
// WorkInProgress,
|
||||
// EngineeringReady,
|
||||
// Live,
|
||||
// Deprecated,
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ComponentScope {
|
||||
Collaboration,
|
||||
|
@ -241,24 +249,30 @@ pub struct ComponentExample {
|
|||
impl RenderOnce for ComponentExample {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.pt_2()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.child(self.variant_name.clone())
|
||||
.text_size(rems(1.25))
|
||||
.text_color(cx.theme().colors().text),
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.child(self.variant_name.clone())
|
||||
.text_size(rems(1.0))
|
||||
.text_color(cx.theme().colors().text),
|
||||
)
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(rems(0.875))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(description.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(rems(0.9375))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(description.clone()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
@ -268,11 +282,11 @@ impl RenderOnce for ComponentExample {
|
|||
.justify_center()
|
||||
.p_8()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(pattern_slash(
|
||||
cx.theme().colors().surface_background.opacity(0.5),
|
||||
24.0,
|
||||
24.0,
|
||||
12.0,
|
||||
12.0,
|
||||
))
|
||||
.shadow_sm()
|
||||
.child(self.element),
|
||||
|
|
|
@ -16,12 +16,16 @@ default = []
|
|||
|
||||
[dependencies]
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
gpui.workspace = true
|
||||
languages.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
notifications.workspace = true
|
||||
collections.workspace = true
|
||||
ui_input.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
db.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
//!
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
mod persistence;
|
||||
|
||||
use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -9,24 +11,27 @@ use client::UserStore;
|
|||
use component::{ComponentId, ComponentMetadata, components};
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
|
||||
uniform_list,
|
||||
};
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
|
||||
use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
|
||||
use languages::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use persistence::COMPONENT_PREVIEW_DB;
|
||||
use project::Project;
|
||||
use ui::{Divider, ListItem, ListSubHeader, prelude::*};
|
||||
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
|
||||
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{AppState, ItemId, SerializableItem};
|
||||
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
workspace::register_serializable_item::<ComponentPreview>(cx);
|
||||
|
||||
let app_state = app_state.clone();
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, cx| {
|
||||
cx.observe_new(move |workspace: &mut Workspace, _window, cx| {
|
||||
let app_state = app_state.clone();
|
||||
let weak_workspace = cx.entity().downgrade();
|
||||
|
||||
|
@ -44,6 +49,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
user_store,
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -64,13 +70,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
enum PreviewEntry {
|
||||
AllComponents,
|
||||
Separator,
|
||||
Component(ComponentMetadata),
|
||||
Component(ComponentMetadata, Option<Vec<usize>>),
|
||||
SectionHeader(SharedString),
|
||||
}
|
||||
|
||||
impl From<ComponentMetadata> for PreviewEntry {
|
||||
fn from(component: ComponentMetadata) -> Self {
|
||||
PreviewEntry::Component(component)
|
||||
PreviewEntry::Component(component, None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +94,7 @@ enum PreviewPage {
|
|||
}
|
||||
|
||||
struct ComponentPreview {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
focus_handle: FocusHandle,
|
||||
_view_scroll_handle: ScrollHandle,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
|
@ -99,6 +106,8 @@ struct ComponentPreview {
|
|||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
filter_editor: Entity<SingleLineInput>,
|
||||
filter_text: String,
|
||||
}
|
||||
|
||||
impl ComponentPreview {
|
||||
|
@ -108,11 +117,14 @@ impl ComponentPreview {
|
|||
user_store: Entity<UserStore>,
|
||||
selected_index: impl Into<Option<usize>>,
|
||||
active_page: Option<PreviewPage>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let sorted_components = components().all_sorted();
|
||||
let selected_index = selected_index.into().unwrap_or(0);
|
||||
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
|
||||
let filter_editor =
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "Find components or usages…"));
|
||||
|
||||
let component_list = ListState::new(
|
||||
sorted_components.len(),
|
||||
|
@ -132,6 +144,7 @@ impl ComponentPreview {
|
|||
);
|
||||
|
||||
let mut component_preview = Self {
|
||||
workspace_id: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
_view_scroll_handle: ScrollHandle::new(),
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
|
@ -143,6 +156,8 @@ impl ComponentPreview {
|
|||
components: sorted_components,
|
||||
component_list,
|
||||
cursor_index: selected_index,
|
||||
filter_editor,
|
||||
filter_text: String::new(),
|
||||
};
|
||||
|
||||
if component_preview.cursor_index > 0 {
|
||||
|
@ -154,6 +169,13 @@ impl ComponentPreview {
|
|||
component_preview
|
||||
}
|
||||
|
||||
pub fn active_page_id(&self, _cx: &App) -> ActivePageId {
|
||||
match &self.active_page {
|
||||
PreviewPage::AllComponents => ActivePageId::default(),
|
||||
PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
self.component_list.scroll_to_reveal_item(ix);
|
||||
self.cursor_index = ix;
|
||||
|
@ -162,6 +184,7 @@ impl ComponentPreview {
|
|||
|
||||
fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
|
||||
self.active_page = page;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -169,20 +192,94 @@ impl ComponentPreview {
|
|||
self.components[ix].clone()
|
||||
}
|
||||
|
||||
fn filtered_components(&self) -> Vec<ComponentMetadata> {
|
||||
if self.filter_text.is_empty() {
|
||||
return self.components.clone();
|
||||
}
|
||||
|
||||
let filter = self.filter_text.to_lowercase();
|
||||
self.components
|
||||
.iter()
|
||||
.filter(|component| {
|
||||
let component_name = component.name().to_lowercase();
|
||||
let scope_name = component.scope().to_string().to_lowercase();
|
||||
let description = component
|
||||
.description()
|
||||
.map(|d| d.to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
component_name.contains(&filter)
|
||||
|| scope_name.contains(&filter)
|
||||
|| description.contains(&filter)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn scope_ordered_entries(&self) -> Vec<PreviewEntry> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut scope_groups: HashMap<ComponentScope, Vec<ComponentMetadata>> = HashMap::default();
|
||||
let mut scope_groups: HashMap<
|
||||
ComponentScope,
|
||||
Vec<(ComponentMetadata, Option<Vec<usize>>)>,
|
||||
> = HashMap::default();
|
||||
let lowercase_filter = self.filter_text.to_lowercase();
|
||||
|
||||
for component in &self.components {
|
||||
scope_groups
|
||||
.entry(component.scope())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(component.clone());
|
||||
if self.filter_text.is_empty() {
|
||||
scope_groups
|
||||
.entry(component.scope())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((component.clone(), None));
|
||||
continue;
|
||||
}
|
||||
|
||||
// let full_component_name = component.name();
|
||||
let scopeless_name = component.scopeless_name();
|
||||
let scope_name = component.scope().to_string();
|
||||
let description = component.description().unwrap_or_default();
|
||||
|
||||
let lowercase_scopeless = scopeless_name.to_lowercase();
|
||||
let lowercase_scope = scope_name.to_lowercase();
|
||||
let lowercase_desc = description.to_lowercase();
|
||||
|
||||
if lowercase_scopeless.contains(&lowercase_filter) {
|
||||
if let Some(index) = lowercase_scopeless.find(&lowercase_filter) {
|
||||
let end = index + lowercase_filter.len();
|
||||
|
||||
if end <= scopeless_name.len() {
|
||||
let mut positions = Vec::new();
|
||||
for i in index..end {
|
||||
if scopeless_name.is_char_boundary(i) {
|
||||
positions.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if !positions.is_empty() {
|
||||
scope_groups
|
||||
.entry(component.scope())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((component.clone(), Some(positions)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lowercase_scopeless.contains(&lowercase_filter)
|
||||
|| lowercase_scope.contains(&lowercase_filter)
|
||||
|| lowercase_desc.contains(&lowercase_filter)
|
||||
{
|
||||
scope_groups
|
||||
.entry(component.scope())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((component.clone(), None));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the components in each group
|
||||
for components in scope_groups.values_mut() {
|
||||
components.sort_by_key(|c| c.name().to_lowercase());
|
||||
components.sort_by_key(|(c, _)| c.sort_name());
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
@ -204,10 +301,10 @@ impl ComponentPreview {
|
|||
if !components.is_empty() {
|
||||
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
||||
let mut sorted_components = components;
|
||||
sorted_components.sort_by_key(|component| component.sort_name());
|
||||
sorted_components.sort_by_key(|(component, _)| component.sort_name());
|
||||
|
||||
for component in sorted_components {
|
||||
entries.push(PreviewEntry::Component(component));
|
||||
for (component, positions) in sorted_components {
|
||||
entries.push(PreviewEntry::Component(component, positions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -219,10 +316,10 @@ impl ComponentPreview {
|
|||
entries.push(PreviewEntry::Separator);
|
||||
entries.push(PreviewEntry::SectionHeader("Uncategorized".into()));
|
||||
let mut sorted_components = components.clone();
|
||||
sorted_components.sort_by_key(|c| c.sort_name());
|
||||
sorted_components.sort_by_key(|(c, _)| c.sort_name());
|
||||
|
||||
for component in sorted_components {
|
||||
entries.push(PreviewEntry::Component(component.clone()));
|
||||
for (component, positions) in sorted_components {
|
||||
entries.push(PreviewEntry::Component(component, positions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,14 +334,33 @@ impl ComponentPreview {
|
|||
cx: &Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
match entry {
|
||||
PreviewEntry::Component(component_metadata) => {
|
||||
PreviewEntry::Component(component_metadata, highlight_positions) => {
|
||||
let id = component_metadata.id();
|
||||
let selected = self.active_page == PreviewPage::Component(id.clone());
|
||||
let name = component_metadata.scopeless_name();
|
||||
|
||||
ListItem::new(ix)
|
||||
.child(
|
||||
Label::new(component_metadata.scopeless_name().clone())
|
||||
.color(Color::Default),
|
||||
)
|
||||
.child(if let Some(_positions) = highlight_positions {
|
||||
let name_lower = name.to_lowercase();
|
||||
let filter_lower = self.filter_text.to_lowercase();
|
||||
let valid_positions = if let Some(start) = name_lower.find(&filter_lower) {
|
||||
let end = start + filter_lower.len();
|
||||
(start..end).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if valid_positions.is_empty() {
|
||||
Label::new(name.clone())
|
||||
.color(Color::Default)
|
||||
.into_any_element()
|
||||
} else {
|
||||
HighlightedLabel::new(name.clone(), valid_positions).into_any_element()
|
||||
}
|
||||
} else {
|
||||
Label::new(name.clone())
|
||||
.color(Color::Default)
|
||||
.into_any_element()
|
||||
})
|
||||
.selectable(true)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
|
@ -282,20 +398,70 @@ impl ComponentPreview {
|
|||
}
|
||||
|
||||
fn update_component_list(&mut self, cx: &mut Context<Self>) {
|
||||
let new_len = self.scope_ordered_entries().len();
|
||||
let entries = self.scope_ordered_entries();
|
||||
let new_len = entries.len();
|
||||
let weak_entity = cx.entity().downgrade();
|
||||
|
||||
if new_len > 0 {
|
||||
self.nav_scroll_handle
|
||||
.scroll_to_item(0, ScrollStrategy::Top);
|
||||
}
|
||||
|
||||
let filtered_components = self.filtered_components();
|
||||
|
||||
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
|
||||
if let PreviewPage::Component(ref component_id) = self.active_page {
|
||||
let component_still_visible = filtered_components
|
||||
.iter()
|
||||
.any(|component| component.id() == *component_id);
|
||||
|
||||
if !component_still_visible {
|
||||
if !filtered_components.is_empty() {
|
||||
let first_component = &filtered_components[0];
|
||||
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
|
||||
} else {
|
||||
self.set_active_page(PreviewPage::AllComponents, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component_list = ListState::new(
|
||||
filtered_components.len(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
{
|
||||
let components = filtered_components.clone();
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
if ix >= components.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let component = &components[ix];
|
||||
this.render_preview(component, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let new_list = ListState::new(
|
||||
new_len,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
move |ix, window, cx| {
|
||||
if ix >= entries.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
let entry = &entries[ix];
|
||||
|
||||
weak_entity
|
||||
.update(cx, |this, cx| match entry {
|
||||
PreviewEntry::Component(component) => this
|
||||
PreviewEntry::Component(component, _) => this
|
||||
.render_preview(component, window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::SectionHeader(shared_string) => this
|
||||
|
@ -309,6 +475,7 @@ impl ComponentPreview {
|
|||
);
|
||||
|
||||
self.component_list = new_list;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
fn render_scope_header(
|
||||
|
@ -377,16 +544,27 @@ impl ComponentPreview {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_all_components(&self) -> impl IntoElement {
|
||||
fn render_all_components(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("component-list")
|
||||
.px_8()
|
||||
.pt_4()
|
||||
.size_full()
|
||||
.child(
|
||||
list(self.component_list.clone())
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto),
|
||||
if self.filtered_components().is_empty() && !self.filter_text.is_empty() {
|
||||
div()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!("No components matching '{}'.", self.filter_text))
|
||||
.into_any_element()
|
||||
} else {
|
||||
list(self.component_list.clone())
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
|
||||
.into_any_element()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -432,6 +610,19 @@ impl ComponentPreview {
|
|||
|
||||
impl Render for ComponentPreview {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// TODO: move this into the struct
|
||||
let current_filter = self.filter_editor.update(cx, |input, cx| {
|
||||
if input.is_empty(cx) {
|
||||
String::new()
|
||||
} else {
|
||||
input.editor().read(cx).text(cx).to_string()
|
||||
}
|
||||
});
|
||||
|
||||
if current_filter != self.filter_text {
|
||||
self.filter_text = current_filter;
|
||||
self.update_component_list(cx);
|
||||
}
|
||||
let sidebar_entries = self.scope_ordered_entries();
|
||||
let active_page = self.active_page.clone();
|
||||
|
||||
|
@ -449,14 +640,22 @@ impl Render for ComponentPreview {
|
|||
.border_color(cx.theme().colors().border)
|
||||
.h_full()
|
||||
.child(
|
||||
uniform_list(
|
||||
gpui::uniform_list(
|
||||
cx.entity().clone(),
|
||||
"component-nav",
|
||||
sidebar_entries.len(),
|
||||
move |this, range, _window, cx| {
|
||||
range
|
||||
.map(|ix| {
|
||||
this.render_sidebar_entry(ix, &sidebar_entries[ix], cx)
|
||||
.filter_map(|ix| {
|
||||
if ix < sidebar_entries.len() {
|
||||
Some(this.render_sidebar_entry(
|
||||
ix,
|
||||
&sidebar_entries[ix],
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
|
@ -481,12 +680,29 @@ impl Render for ComponentPreview {
|
|||
),
|
||||
),
|
||||
)
|
||||
.child(match active_page {
|
||||
PreviewPage::AllComponents => self.render_all_components().into_any_element(),
|
||||
PreviewPage::Component(id) => self
|
||||
.render_component_page(&id, window, cx)
|
||||
.into_any_element(),
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.id("content-area")
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.filter_editor.clone()),
|
||||
)
|
||||
.child(match active_page {
|
||||
PreviewPage::AllComponents => {
|
||||
self.render_all_components(cx).into_any_element()
|
||||
}
|
||||
PreviewPage::Component(id) => self
|
||||
.render_component_page(&id, window, cx)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -498,6 +714,21 @@ impl Focusable for ComponentPreview {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ActivePageId(pub String);
|
||||
|
||||
impl Default for ActivePageId {
|
||||
fn default() -> Self {
|
||||
ActivePageId("AllComponents".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComponentId> for ActivePageId {
|
||||
fn from(id: ComponentId) -> Self {
|
||||
ActivePageId(id.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for ComponentPreview {
|
||||
type Event = ItemEvent;
|
||||
|
||||
|
@ -516,7 +747,7 @@ impl Item for ComponentPreview {
|
|||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<gpui::Entity<Self>>
|
||||
where
|
||||
|
@ -535,6 +766,7 @@ impl Item for ComponentPreview {
|
|||
user_store,
|
||||
selected_index,
|
||||
Some(active_page),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
|
@ -543,6 +775,15 @@ impl Item for ComponentPreview {
|
|||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace_id = workspace.database_id();
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializableItem for ComponentPreview {
|
||||
|
@ -553,26 +794,53 @@ impl SerializableItem for ComponentPreview {
|
|||
fn deserialize(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<Self>>> {
|
||||
let deserialized_active_page =
|
||||
match COMPONENT_PREVIEW_DB.get_active_page(item_id, workspace_id) {
|
||||
Ok(page) => {
|
||||
if let Some(page) = page {
|
||||
ActivePageId(page)
|
||||
} else {
|
||||
ActivePageId::default()
|
||||
}
|
||||
}
|
||||
Err(_) => ActivePageId::default(),
|
||||
};
|
||||
|
||||
let user_store = project.read(cx).user_store().clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 {
|
||||
Some(PreviewPage::default())
|
||||
} else {
|
||||
let component_str = deserialized_active_page.0;
|
||||
let component_registry = components();
|
||||
let all_components = component_registry.all();
|
||||
let found_component = all_components.iter().find(|c| c.id().0 == component_str);
|
||||
|
||||
if let Some(component) = found_component {
|
||||
Some(PreviewPage::Component(component.id().clone()))
|
||||
} else {
|
||||
Some(PreviewPage::default())
|
||||
}
|
||||
};
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let user_store = user_store.clone();
|
||||
let language_registry = language_registry.clone();
|
||||
let weak_workspace = workspace.clone();
|
||||
cx.update(|_, cx| {
|
||||
cx.update(move |window, cx| {
|
||||
Ok(cx.new(|cx| {
|
||||
ComponentPreview::new(
|
||||
weak_workspace,
|
||||
language_registry,
|
||||
user_store,
|
||||
None,
|
||||
None,
|
||||
preview_page,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
|
@ -581,34 +849,41 @@ impl SerializableItem for ComponentPreview {
|
|||
}
|
||||
|
||||
fn cleanup(
|
||||
_workspace_id: WorkspaceId,
|
||||
_alive_items: Vec<ItemId>,
|
||||
workspace_id: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
// window.spawn(cx, |_| {
|
||||
// ...
|
||||
// })
|
||||
cx.background_spawn(async move {
|
||||
COMPONENT_PREVIEW_DB
|
||||
.delete_unloaded_items(workspace_id, alive_items)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
&mut self,
|
||||
_workspace: &mut Workspace,
|
||||
_item_id: ItemId,
|
||||
item_id: ItemId,
|
||||
_closing: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<gpui::Result<()>>> {
|
||||
// TODO: Serialize the active index so we can re-open to the same place
|
||||
None
|
||||
let active_page = self.active_page_id(cx);
|
||||
let workspace_id = self.workspace_id?;
|
||||
Some(cx.background_spawn(async move {
|
||||
COMPONENT_PREVIEW_DB
|
||||
.save_active_page(item_id, workspace_id, active_page.0)
|
||||
.await
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_serialize(&self, _event: &Self::Event) -> bool {
|
||||
false
|
||||
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||
matches!(event, ItemEvent::UpdateTab)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use language registry to allow rendering markdown
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComponentPreviewPage {
|
||||
// languages: Arc<LanguageRegistry>,
|
||||
|
|
73
crates/component_preview/src/persistence.rs
Normal file
73
crates/component_preview/src/persistence.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use anyhow::Result;
|
||||
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||
|
||||
define_connection! {
|
||||
pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb<WorkspaceDb> =
|
||||
&[sql!(
|
||||
CREATE TABLE component_previews (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
active_page_id TEXT,
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
impl ComponentPreviewDb {
|
||||
pub async fn save_active_page(
|
||||
&self,
|
||||
item_id: ItemId,
|
||||
workspace_id: WorkspaceId,
|
||||
active_page_id: String,
|
||||
) -> Result<()> {
|
||||
let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
active_page_id = ?3";
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&item_id, 1)?;
|
||||
next_index = statement.bind(&workspace_id, next_index)?;
|
||||
statement.bind(&active_page_id, next_index)?;
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn get_active_page(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<String>> {
|
||||
SELECT active_page_id
|
||||
FROM component_previews
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM component_previews WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -236,53 +236,30 @@ impl Component for Avatar {
|
|||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![
|
||||
example_group(vec![
|
||||
single_example("Default", Avatar::new(example_avatar).into_any_element()),
|
||||
single_example(
|
||||
"Grayscale",
|
||||
Avatar::new(example_avatar)
|
||||
.grayscale(true)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Border",
|
||||
Avatar::new(example_avatar)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.into_any_element(),
|
||||
).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."),
|
||||
]),
|
||||
example_group_with_title(
|
||||
"Sizes",
|
||||
vec![
|
||||
single_example(
|
||||
"Default",
|
||||
Avatar::new(example_avatar).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Small",
|
||||
Avatar::new(example_avatar).size(px(24.)).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Large",
|
||||
Avatar::new(example_avatar).size(px(48.)).into_any_element(),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Styles",
|
||||
vec![
|
||||
single_example(
|
||||
"Default",
|
||||
Avatar::new(example_avatar).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Grayscale",
|
||||
Avatar::new(example_avatar)
|
||||
.grayscale(true)
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"With Border",
|
||||
Avatar::new(example_avatar)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Audio Status",
|
||||
"Indicator Styles",
|
||||
vec![
|
||||
single_example(
|
||||
"Muted",
|
||||
Avatar::new(example_avatar)
|
||||
.indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
|
||||
.into_any_element(),
|
||||
),
|
||||
).description("Indicates the collaborator's mic is muted."),
|
||||
single_example(
|
||||
"Deafened",
|
||||
Avatar::new(example_avatar)
|
||||
|
@ -290,28 +267,23 @@ impl Component for Avatar {
|
|||
AudioStatus::Deafened,
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Availability",
|
||||
vec![
|
||||
).description("Indicates that both the collaborator's mic and audio are muted."),
|
||||
single_example(
|
||||
"Free",
|
||||
"Availability: Free",
|
||||
Avatar::new(example_avatar)
|
||||
.indicator(AvatarAvailabilityIndicator::new(
|
||||
CollaboratorAvailability::Free,
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
).description("Indicates that the person is free, usually meaning they are not in a call."),
|
||||
single_example(
|
||||
"Busy",
|
||||
"Availability: Busy",
|
||||
Avatar::new(example_avatar)
|
||||
.indicator(AvatarAvailabilityIndicator::new(
|
||||
CollaboratorAvailability::Busy,
|
||||
))
|
||||
.into_any_element(),
|
||||
),
|
||||
).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."),
|
||||
],
|
||||
),
|
||||
])
|
||||
|
|
|
@ -451,7 +451,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
|||
|
||||
impl Component for KeyBinding {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Input
|
||||
ComponentScope::Typography
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue