feat: Implement hierarchical sidebar with projects and tags

Add complete sidebar implementation with:
- ProjectTree model using arena allocator for hierarchical projects
- FilterState model for global filter state management
- Sidebar view with independent scrollable sections
- Project tree with expand/collapse functionality
- Tag selection with toggle support
- Observable filter state shared across components
This commit is contained in:
Ignacio Perez
2025-12-25 10:55:58 -03:00
parent b4e0d17391
commit 7842b16ccf
8 changed files with 859 additions and 46 deletions
+78 -45
View File
@@ -1,14 +1,13 @@
use gpui::prelude::*;
use std::sync::Arc;
use crate::components::{
self,
input::{Input, Suggestion},
};
use crate::models::{FilterState, ProjectTree};
use crate::theme::ActiveTheme;
use crate::view::sidebar::{Sidebar, TagItem};
use gpui::div;
pub(crate) struct App {
input: gpui::Entity<Input>,
current_text: gpui::SharedString,
sidebar: gpui::Entity<Sidebar>,
filter_state: gpui::Entity<FilterState>,
}
impl gpui::Render for App {
@@ -17,12 +16,26 @@ impl gpui::Render for App {
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
components::panel::Panel::new()
.child(self.input.clone())
.child(components::label::Label::new(format!(
"Text: {}",
&self.current_text
)))
let theme = _cx.theme();
div()
.flex()
.size_full()
.bg(theme.background)
.child(div().w(gpui::px(250.)).h_full().child(self.sidebar.clone()))
.child(
div()
.flex_1()
.h_full()
.flex()
.items_center()
.justify_center()
.child(
div()
.text_color(theme.muted)
.child("Main panel - TaskTable will go here"),
),
)
}
}
@@ -36,42 +49,62 @@ impl App {
gpui::WindowOptions::default(),
|_window: &mut gpui::Window, app: &mut gpui::App| {
app.new(|cx: &mut gpui::Context<'_, App>| {
let input = cx.new(|cx| {
Input::new("input", cx, "Type here...").with_suggest(Arc::new(|text| {
let suggestions = vec![
("project:", "Filter by project"),
("+work", "Tag: work"),
("+personal", "Tag: personal"),
("due:today", "Tasks for today"),
("due:tomorrow", "Tasks for tomorrow"),
("priority:H", "Priority high"),
("priority:M", "Priority medium"),
("priority:L", "Priority low"),
];
let filter_state = cx.new(|_cx| FilterState::new());
suggestions
.into_iter()
.filter(|(insert, _)| {
text.is_empty()
|| insert.to_lowercase().contains(&text.to_lowercase())
})
.map(|(insert, label)| {
Suggestion::new(format!("{} - {}", insert, label), insert)
})
.collect()
}))
let mut project_tree = ProjectTree::new();
project_tree.build_from_projects(&[
("Work.Backend.API".to_string(), 5),
("Work.Backend.DB".to_string(), 7),
("Work.Frontend.React".to_string(), 3),
("Work.Frontend.Styling".to_string(), 2),
("Home.Kitchen".to_string(), 4),
("Home.Garden".to_string(), 2),
("ignis.v0.1.phase0".to_string(), 15),
("free-ai".to_string(), 2),
]);
project_tree.expand_path("Work");
project_tree.expand_path("Work.Backend");
let tags = vec![
TagItem {
name: "parser".to_string(),
task_count: 8,
},
TagItem {
name: "cli".to_string(),
task_count: 5,
},
TagItem {
name: "testing".to_string(),
task_count: 6,
},
TagItem {
name: "diagnostics".to_string(),
task_count: 7,
},
TagItem {
name: "analyzer".to_string(),
task_count: 3,
},
];
let sidebar = cx.new(|cx| {
Sidebar::new(project_tree, tags, filter_state.clone(), cx)
.on_filter_change(|filter, _window, _cx| {
println!("Filter changed:");
if let Some(ref project) = filter.selected_project {
println!(" Project: {}", project);
} else {
println!(" Project: All");
}
println!(" Active tags: {:?}", filter.active_tags);
})
});
cx.observe(&input, |this, input, cx| {
let value = input.read(cx).value().to_string();
this.current_text = value.into();
cx.notify();
})
.detach();
App {
input,
current_text: "".into(),
sidebar,
filter_state,
}
})
},