diff --git a/src/app.rs b/src/app.rs index 1c01dd4..f23d1ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,9 +20,9 @@ impl gpui::Render for App { fn render( &mut self, _window: &mut gpui::Window, - _cx: &mut gpui::Context, + cx: &mut gpui::Context, ) -> impl gpui::IntoElement { - let theme = _cx.theme(); + let theme = cx.theme(); let content = div() .flex() diff --git a/src/components/button/dropdown.rs b/src/components/button/dropdown.rs index 708fbfe..458499e 100644 --- a/src/components/button/dropdown.rs +++ b/src/components/button/dropdown.rs @@ -3,17 +3,31 @@ use std::sync::Arc; use gpui::prelude::*; use crate::components::button::Button; +use crate::components::label::Label; use crate::theme::ActiveTheme; #[derive(Clone, Debug)] pub struct DropdownItem { pub label: gpui::SharedString, + pub value: gpui::SharedString, } impl DropdownItem { pub fn new(label: impl Into) -> Self { + let label = label.into(); + Self { + value: label.clone(), + label, + } + } + + pub fn with_value( + label: impl Into, + value: impl Into, + ) -> Self { Self { label: label.into(), + value: value.into(), } } } @@ -27,6 +41,7 @@ pub struct Dropdown { disabled: bool, loading: bool, placeholder: gpui::SharedString, + label_prefix: Option, on_select: Option) + Send + Sync>>, } @@ -41,6 +56,7 @@ impl Dropdown { disabled: false, loading: false, placeholder: "Seleccionar".into(), + label_prefix: None, on_select: None, } } @@ -68,6 +84,11 @@ impl Dropdown { self } + pub fn label_prefix(mut self, prefix: impl Into) -> Self { + self.label_prefix = Some(prefix.into()); + self + } + pub fn selected_index(mut self, index: usize) -> Self { self.selected_index = Some(index); self @@ -108,6 +129,23 @@ impl Dropdown { .and_then(|index| self.items.get(index).map(|item| item.label.clone())) } + pub fn set_selected_index(&mut self, index: usize, cx: &mut gpui::Context) { + if index < self.items.len() { + self.selected_index = Some(index); + cx.notify(); + } + } + + pub fn set_items(&mut self, items: Vec, cx: &mut gpui::Context) { + self.items = items; + if let Some(selected) = self.selected_index { + if selected >= self.items.len() { + self.selected_index = None; + } + } + cx.notify(); + } + fn toggle_open(&mut self, cx: &mut gpui::Context) { if self.disabled || self.loading || self.items.is_empty() { return; @@ -168,15 +206,21 @@ impl Dropdown { let is_selected = self.selected_index == Some(index); let mut row = gpui::div() .id(index) + .w_full() .px_2() .py_1() + .text_sm() + .whitespace_nowrap() + .bg(theme.background) .text_color(if is_selected { theme.selection_foreground } else { theme.foreground }) .when(is_selected, |el| el.bg(theme.selection)) - .hover(|s: gpui::StyleRefinement| s.bg(theme.selection)) + .when(!is_disabled, |el| { + el.hover(|s: gpui::StyleRefinement| s.bg(theme.selection)) + }) .child(item.label.clone()); if is_disabled { @@ -198,13 +242,17 @@ impl Dropdown { .absolute() .top_full() .left_0() - .right_0() + .min_w(gpui::rems(12.0)) + .min_w_full() .mt_1() + .occlude() + .p_1() .border_1() .border_color(theme.border) - .bg(theme.panel) + .bg(theme.background) .rounded_md() .overflow_hidden() + .shadow_lg() .children(items) .into_any_element() } @@ -216,28 +264,60 @@ impl gpui::Render for Dropdown { _window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl IntoElement { + let theme = cx.theme(); let is_disabled = self.disabled || self.loading; - let label = self + let disabled = is_disabled || self.items.is_empty(); + let mut label = self .selected_label() .unwrap_or_else(|| self.placeholder.clone()); - - let mut trigger = if let Some(button) = self.button.clone() { - let mut button = button; - button.on_click = None; - button.label = Some(label.clone()); - button.disabled(is_disabled).loading(self.loading) - } else { - Button::label(self.id.clone(), label) - .disabled(is_disabled) - .loading(self.loading) - }; - - if is_disabled || self.items.is_empty() { - trigger = trigger.disabled(true); + if let Some(prefix) = &self.label_prefix { + label = format!("{}: {}", prefix, label).into(); } - let mut trigger_wrap = gpui::div().child(trigger); - if !is_disabled && !self.items.is_empty() { + let arrow = "▾"; + + let trigger: gpui::AnyElement = if let Some(button) = self.button.clone() { + let mut button = button; + button.on_click = None; + button.label = Some(format!("{} {}", label, arrow).into()); + button + .disabled(disabled) + .loading(self.loading) + .bg(theme.background) + .border_color(theme.border) + .text_color(theme.foreground) + .into_any_element() + } else { + let mut trigger = gpui::div() + .flex() + .items_center() + .gap_2() + .px_3() + .py_2() + .rounded_md() + .border_1() + .border_color(theme.border) + .bg(theme.background) + .text_sm() + .text_color(theme.foreground) + .child(Label::new(label.clone())) + .child(Label::new(arrow).text_color(theme.muted)); + + if disabled { + trigger = trigger.text_color(theme.muted).cursor_not_allowed(); + } else { + trigger = trigger + .cursor_pointer() + .hover(|s: gpui::StyleRefinement| s.bg(theme.selection)); + } + + trigger.into_any_element() + }; + + let mut trigger_wrap = gpui::div() + .min_w(gpui::rems(12.0)) + .child(trigger); + if !disabled { trigger_wrap = trigger_wrap.on_mouse_down( gpui::MouseButton::Left, cx.listener(Self::handle_trigger_mouse_down), diff --git a/src/components/button/mod.rs b/src/components/button/mod.rs index a2e3560..efd708b 100644 --- a/src/components/button/mod.rs +++ b/src/components/button/mod.rs @@ -7,6 +7,7 @@ use gpui::{ use std::sync::Arc; use crate::components::icon::{Icon, IconSize}; +use crate::components::label::Label; use crate::theme::ActiveTheme; fn darken(color: gpui::Rgba, amount: f32) -> gpui::Rgba { @@ -264,7 +265,7 @@ impl RenderOnce for Button { } if let Some(label) = self.label { - base = base.child(label); + base = base.child(Label::new(label).text_color(fg)); } if self.loading { diff --git a/src/components/panel.rs b/src/components/panel.rs index e2f3b25..f38a7a1 100644 --- a/src/components/panel.rs +++ b/src/components/panel.rs @@ -78,7 +78,8 @@ impl gpui::RenderOnce for Panel { let children: Vec = self.content.drain(..).collect(); - gpui::div() + let mut base = gpui::div() + .relative() .size_full() .bg(theme.panel) .border(gpui::px(self.border)) @@ -87,6 +88,10 @@ impl gpui::RenderOnce for Panel { .p(gpui::px(self.padding)) .overflow_hidden() .children(header) - .children(children) + .children(children); + + base.style().refine(&self.style); + + base } } diff --git a/src/models/filter_state.rs b/src/models/filter_state.rs index 4963226..9b96421 100644 --- a/src/models/filter_state.rs +++ b/src/models/filter_state.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +use chrono::NaiveDate; + #[derive(Debug, Clone, Default)] pub struct FilterState { pub selected_project: Option, @@ -45,6 +47,17 @@ impl StatusFilter { Self::Deleted, ] } + + pub fn from_index(index: usize) -> Self { + Self::all_variants().get(index).copied().unwrap_or_default() + } + + pub fn to_index(&self) -> usize { + Self::all_variants() + .iter() + .position(|v| v == self) + .unwrap_or(0) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -76,6 +89,17 @@ impl PriorityFilter { pub fn all_variants() -> &'static [Self] { &[Self::All, Self::High, Self::Medium, Self::Low, Self::None] } + + pub fn from_index(index: usize) -> Self { + Self::all_variants().get(index).copied().unwrap_or_default() + } + + pub fn to_index(&self) -> usize { + Self::all_variants() + .iter() + .position(|v| v == self) + .unwrap_or(0) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -85,6 +109,7 @@ pub enum DueFilter { Today, ThisWeek, NoDate, + OnDate(NaiveDate), } impl Default for DueFilter { @@ -94,16 +119,6 @@ impl Default for DueFilter { } impl DueFilter { - pub fn as_str(&self) -> &'static str { - match self { - Self::All => "All", - Self::Overdue => "Overdue", - Self::Today => "Today", - Self::ThisWeek => "This Week", - Self::NoDate => "No Date", - } - } - pub fn all_variants() -> &'static [Self] { &[ Self::All, @@ -113,6 +128,54 @@ impl DueFilter { Self::NoDate, ] } + + pub fn from_index(index: usize) -> Self { + Self::all_variants().get(index).copied().unwrap_or_default() + } + + pub fn to_index(&self) -> usize { + Self::all_variants() + .iter() + .position(|v| v == self) + .unwrap_or(0) + } + + pub fn label(&self) -> String { + match self { + Self::All => "All".to_string(), + Self::Overdue => "Overdue".to_string(), + Self::Today => "Today".to_string(), + Self::ThisWeek => "This Week".to_string(), + Self::NoDate => "No Date".to_string(), + Self::OnDate(date) => date.format("%d-%m-%Y").to_string(), + } + } + + pub fn value_key(&self) -> String { + match self { + Self::All => "all".to_string(), + Self::Overdue => "overdue".to_string(), + Self::Today => "today".to_string(), + Self::ThisWeek => "this_week".to_string(), + Self::NoDate => "none".to_string(), + Self::OnDate(date) => format!("date:{}", date.format("%Y-%m-%d")), + } + } + + pub fn from_value(value: &str) -> Option { + match value { + "all" => Some(Self::All), + "overdue" => Some(Self::Overdue), + "today" => Some(Self::Today), + "this_week" => Some(Self::ThisWeek), + "none" => Some(Self::NoDate), + _ => value.strip_prefix("date:").and_then(|date| { + NaiveDate::parse_from_str(date, "%Y-%m-%d") + .ok() + .map(Self::OnDate) + }), + } + } } impl FilterState { diff --git a/src/task/filter.rs b/src/task/filter.rs index 6926d5b..fbd048e 100644 --- a/src/task/filter.rs +++ b/src/task/filter.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, NaiveDate, Utc}; use super::model::{Task, TaskPriority, TaskStatus}; use crate::models::{DueFilter, FilterState, PriorityFilter, StatusFilter}; @@ -20,6 +20,7 @@ pub enum DueDateFilter { NoDate, Before(DateTime), After(DateTime), + OnDate(NaiveDate), } #[derive(Debug, Clone, Default)] @@ -112,6 +113,7 @@ impl From<&FilterState> for TaskFilter { DueFilter::Today => Some(DueDateFilter::Today), DueFilter::ThisWeek => Some(DueDateFilter::ThisWeek), DueFilter::NoDate => Some(DueDateFilter::NoDate), + DueFilter::OnDate(date) => Some(DueDateFilter::OnDate(date)), }; if !state.search_text.is_empty() { @@ -228,6 +230,11 @@ impl TaskFilter { return false; } } + DueDateFilter::OnDate(date) => { + if !task.due.map(|d| d.date_naive() == *date).unwrap_or(false) { + return false; + } + } } } diff --git a/src/view/sidebar.rs b/src/view/sidebar.rs index 149829c..81a8986 100644 --- a/src/view/sidebar.rs +++ b/src/view/sidebar.rs @@ -71,6 +71,22 @@ impl Sidebar { cx.notify(); } + fn handle_clear_tags(&mut self, _window: &mut Window, cx: &mut Context) { + self.filter_state.update(cx, |filter, cx| { + filter.active_tags.clear(); + cx.notify(); + }); + cx.notify(); + } + + fn handle_clear_project(&mut self, _window: &mut Window, cx: &mut Context) { + self.filter_state.update(cx, |filter, cx| { + filter.selected_project = None; + cx.notify(); + }); + cx.notify(); + } + fn render_projects(&self, cx: &mut Context) -> Vec
{ let theme = cx.theme(); let filter = self.filter_state.read(cx); @@ -259,6 +275,9 @@ impl Render for Sidebar { let theme = cx.theme().clone(); let projects = self.render_projects(cx); let tags = self.render_tags(cx); + let filter = self.filter_state.read(cx); + let has_project = filter.selected_project.is_some(); + let has_tags = !filter.active_tags.is_empty(); Panel::new("Sidebar").border(1.0).padding(0.0).child( div() @@ -276,6 +295,9 @@ impl Render for Sidebar { .child( div() .flex_shrink_0() + .flex() + .items_center() + .justify_between() .px_3() .py_2() .border_b_1() @@ -285,7 +307,24 @@ impl Render for Sidebar { .text_sm() .font_weight(gpui::FontWeight::BOLD) .text_color(theme.foreground), - ), + ) + .when(has_project, |this| { + this.child( + div() + .id("clear-project") + .text_xs() + .text_color(theme.muted) + .cursor_pointer() + .hover(|s| s.text_color(theme.accent)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|view, _, window, cx| { + view.handle_clear_project(window, cx); + }), + ) + .child("Clear"), + ) + }), ) .child( div() @@ -308,6 +347,9 @@ impl Render for Sidebar { .child( div() .flex_shrink_0() + .flex() + .items_center() + .justify_between() .px_3() .py_2() .border_b_1() @@ -317,7 +359,24 @@ impl Render for Sidebar { .text_sm() .font_weight(gpui::FontWeight::BOLD) .text_color(theme.foreground), - ), + ) + .when(has_tags, |this| { + this.child( + div() + .id("clear-tags") + .text_xs() + .text_color(theme.muted) + .cursor_pointer() + .hover(|s| s.text_color(theme.accent)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|view, _, window, cx| { + view.handle_clear_tags(window, cx); + }), + ) + .child("Clear"), + ) + }), ) .child( div() diff --git a/src/view/task_table.rs b/src/view/task_table.rs index 8330bbe..fe304ef 100644 --- a/src/view/task_table.rs +++ b/src/view/task_table.rs @@ -1,10 +1,16 @@ use std::cmp::Ordering; +use std::collections::BTreeSet; +use std::sync::Arc; use gpui::prelude::*; use crate::{ - components, - models::FilterState, + components::{ + self, + button::{Dropdown, DropdownItem}, + input::Input, + }, + models::{DueFilter, FilterState, PriorityFilter, StatusFilter}, task::{self, TaskFilter, TaskService}, theme::{self, ActiveTheme}, }; @@ -216,14 +222,93 @@ pub struct TaskTable { selected_page_idx: Option, selected_global_idx: Option, need_reload: bool, + filter_bar_height: gpui::Pixels, + search_input: gpui::Entity, + status_dropdown: gpui::Entity, + priority_dropdown: gpui::Entity, + due_dropdown: gpui::Entity, } impl TaskTable { pub fn new( id: impl Into, filter_state: gpui::Entity, - _cx: &mut gpui::Context, + cx: &mut gpui::Context, ) -> Self { + let search_input = { + let filter_state = filter_state.clone(); + cx.new(|cx| { + Input::new("filter-search", cx, "Search...").with_on_change(Arc::new( + move |value: &str, cx: &mut gpui::Context| { + cx.update_entity(&filter_state, |filter, cx| { + filter.search_text = value.to_string(); + cx.notify(); + }); + }, + )) + }) + }; + + let status_items = StatusFilter::all_variants() + .iter() + .map(|status| DropdownItem::new(status.as_str())) + .collect::>(); + let status_dropdown = { + let filter_state = filter_state.clone(); + cx.new(|_cx| { + Dropdown::new("filter-status") + .items(status_items) + .label_prefix("Status") + .selected_index(StatusFilter::default().to_index()) + .on_select(Arc::new(move |index, _item, cx| { + let selected = StatusFilter::from_index(index); + cx.update_entity(&filter_state, |filter, cx| { + filter.status_filter = selected; + cx.notify(); + }); + })) + }) + }; + + let priority_items = PriorityFilter::all_variants() + .iter() + .map(|priority| DropdownItem::new(priority.as_str())) + .collect::>(); + let priority_dropdown = { + let filter_state = filter_state.clone(); + cx.new(|_cx| { + Dropdown::new("filter-priority") + .items(priority_items) + .label_prefix("Priority") + .selected_index(PriorityFilter::default().to_index()) + .on_select(Arc::new(move |index, _item, cx| { + let selected = PriorityFilter::from_index(index); + cx.update_entity(&filter_state, |filter, cx| { + filter.priority_filter = selected; + cx.notify(); + }); + })) + }) + }; + + let due_dropdown = { + let filter_state = filter_state.clone(); + cx.new(|_cx| { + Dropdown::new("filter-due") + .items(vec![DropdownItem::with_value("All", "all")]) + .label_prefix("Due") + .selected_index(0) + .on_select(Arc::new(move |_index, item, cx| { + let selected = + DueFilter::from_value(item.value.as_ref()).unwrap_or(DueFilter::All); + cx.update_entity(&filter_state, |filter, cx| { + filter.due_filter = selected; + cx.notify(); + }); + })) + }) + }; + Self { id: id.into(), filter_state, @@ -234,6 +319,11 @@ impl TaskTable { selected_page_idx: None, selected_global_idx: None, need_reload: true, + filter_bar_height: gpui::px(52.0), + search_input, + status_dropdown, + priority_dropdown, + due_dropdown, } } @@ -297,14 +387,17 @@ impl TaskTable { pub fn reload_tasks(&mut self, task_service: &mut TaskService, cx: &mut gpui::Context) { let filter_state = self.filter_state.read(cx).clone(); - let task_filter = TaskFilter::from(&filter_state); + let all_tasks = task_service.get_all_tasks().unwrap_or_else(|e| { + log::error!("[TaskTable] Failed to load tasks: {}", e); + vec![] + }); - let filtered_tasks = task_service - .get_filtered_tasks(&task_filter) - .unwrap_or_else(|e| { - log::error!("[TaskTable] Failed to load filtered tasks: {}", e); - vec![] - }); + let task_filter = TaskFilter::from(&filter_state); + let mut due_filter = task_filter.clone(); + due_filter.due_filter = None; + + let filtered_tasks = task_filter.apply(&all_tasks); + let due_tasks = due_filter.apply(&all_tasks); self.cached_tasks = filtered_tasks; self.apply_sort(); @@ -314,6 +407,7 @@ impl TaskTable { self.selected_page_idx = None; self.recalculate_rows(); + self.sync_filter_dropdowns(&due_tasks, &filter_state, cx); self.need_reload = false; @@ -324,6 +418,121 @@ impl TaskTable { self.cached_rows = self.cached_tasks.iter().map(TaskRow::from).collect(); } + fn sync_filter_dropdowns( + &mut self, + due_tasks: &[task::Task], + filter_state: &FilterState, + cx: &mut gpui::Context, + ) { + let status_index = filter_state.status_filter.to_index(); + self.status_dropdown.update(cx, |dropdown, cx| { + dropdown.set_selected_index(status_index, cx); + }); + + let priority_index = filter_state.priority_filter.to_index(); + self.priority_dropdown.update(cx, |dropdown, cx| { + dropdown.set_selected_index(priority_index, cx); + }); + + let mut due_items = Self::build_due_items(due_tasks); + let selected_key = filter_state.due_filter.value_key(); + let mut selected_index = due_items + .iter() + .position(|item| item.value.as_ref() == selected_key); + + if selected_index.is_none() && filter_state.due_filter != DueFilter::All { + if let Some(item) = Self::due_item_from_filter(&filter_state.due_filter) { + due_items.push(item); + selected_index = Some(due_items.len() - 1); + } + } + + self.due_dropdown.update(cx, |dropdown, cx| { + dropdown.set_items(due_items, cx); + if let Some(index) = selected_index { + dropdown.set_selected_index(index, cx); + } + }); + } + + fn build_due_items(tasks: &[task::Task]) -> Vec { + let now = chrono::Utc::now(); + let today = now.date_naive(); + let week_end = now + chrono::Duration::days(7); + + let mut dates = BTreeSet::new(); + let mut has_no_date = false; + let mut has_overdue = false; + let mut has_today = false; + let mut has_this_week = false; + + for task in tasks { + match task.due { + None => { + has_no_date = true; + } + Some(due) => { + let date = due.date_naive(); + dates.insert(date); + if due < now { + has_overdue = true; + } + if date == today { + has_today = true; + } + if due >= now && due <= week_end { + has_this_week = true; + } + } + } + } + + let mut items = vec![DropdownItem::with_value("All", "all")]; + if has_no_date { + items.push(DropdownItem::with_value("No Date", "none")); + } + if has_overdue { + items.push(DropdownItem::with_value("Overdue", "overdue")); + } + if has_today { + items.push(DropdownItem::with_value("Today", "today")); + } + if has_this_week { + items.push(DropdownItem::with_value("This Week", "this_week")); + } + + for date in dates { + if date == today { + continue; + } + let label = Self::format_due_label(date); + let value = format!("date:{}", date.format("%Y-%m-%d")); + items.push(DropdownItem::with_value(label, value)); + } + + items + } + + fn due_item_from_filter(filter: &DueFilter) -> Option { + match filter { + DueFilter::All => None, + DueFilter::OnDate(date) => Some(DropdownItem::with_value( + Self::format_due_label(*date), + filter.value_key(), + )), + _ => Some(DropdownItem::with_value(filter.label(), filter.value_key())), + } + } + + fn format_due_label(date: chrono::NaiveDate) -> String { + let today = chrono::Utc::now().date_naive(); + if date == today { + "Today".to_string() + } else { + date.format("%d-%m-%Y").to_string() + } + } + fn priority_color(&self, row: &TaskRow, cx: &gpui::Context) -> theme::Color { let theme = cx.theme(); @@ -376,6 +585,74 @@ impl TaskTable { cx.notify(); } + fn handle_clear_filters(&mut self, cx: &mut gpui::Context) { + self.search_input.update(cx, |input, cx| { + input.clear(cx); + }); + self.filter_state.update(cx, |filter, cx| { + filter.clear(); + cx.notify(); + }); + } + + fn render_filter_bar(&self, cx: &gpui::Context) -> impl gpui::IntoElement { + let theme = cx.theme(); + let filter = self.filter_state.read(cx); + let has_filters = filter.has_active_filters(); + let view = cx.entity().clone(); + + let bar = gpui::div() + .id("filter-bar") + .flex() + .flex_wrap() + .gap_2() + .items_center() + .px_4() + .py_2() + .bg(theme.panel) + .child( + gpui::div() + .w(gpui::rems(14.0)) + .child(self.search_input.clone()), + ) + .child(self.status_dropdown.clone()) + .child(self.priority_dropdown.clone()) + .child(self.due_dropdown.clone()) + .when(has_filters, |this| { + this.child( + gpui::div() + .id("clear-all-filters") + .px_2() + .py_1() + .rounded_md() + .text_sm() + .text_color(theme.error) + .cursor_pointer() + .hover(|s| s.bg(theme.selection)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|table, _, _, cx| { + table.handle_clear_filters(cx); + }), + ) + .child("✕ Clear"), + ) + }); + + gpui::div().child(bar).on_children_prepainted(move |bounds, _, cx| { + let Some(bounds) = bounds.first() else { + return; + }; + let height = bounds.size.height; + cx.update_entity(&view, |table, cx| { + if table.filter_bar_height != height { + table.filter_bar_height = height; + cx.notify(); + } + }); + }) + } + fn render_header_column( &self, column: SortColumn, @@ -424,9 +701,10 @@ impl TaskTable { .items_center() .gap_2() .px_4() - .py_1() - .border_b_1() + .py_2() + .border_t_1() .border_color(theme.border) + .border_b_1() .bg(theme.panel) .text_sm() .font_weight(gpui::FontWeight::MEDIUM) @@ -632,12 +910,12 @@ impl TaskTable { impl gpui::Render for TaskTable { fn render( &mut self, - window: &mut gpui::Window, + _window: &mut gpui::Window, cx: &mut gpui::Context, ) -> impl gpui::IntoElement { let theme = cx.theme(); - let panel = components::panel::Panel::new(self.id.clone()); + let panel = components::panel::Panel::new(self.id.clone()).flex().flex_col(); if self.need_reload { return panel @@ -656,13 +934,19 @@ impl gpui::Render for TaskTable { .map(|(index, row)| self.render_row(index, row, cx)) .collect(); - panel + let filter_bar = self.render_filter_bar(cx); + let header = self.render_header(cx); + let footer = self.render_footer(cx); + + let body = gpui::div() .flex() .flex_col() - .size_full() + .flex_1() + .min_h_0() .overflow_hidden() .bg(theme.background) - .child(self.render_header(cx)) + .child(gpui::div().h(self.filter_bar_height)) + .child(header) .child( gpui::div() .id("task-table-content") @@ -671,6 +955,17 @@ impl gpui::Render for TaskTable { .overflow_y_scroll() .child(gpui::div().flex().flex_col().children(rows)), ) - .child(self.render_footer(cx)) + .child(footer); + + panel + .child(body) + .child( + gpui::div() + .absolute() + .top_0() + .left_0() + .right_0() + .child(filter_bar), + ) } }