diff --git a/src/app.rs b/src/app.rs index bdde28e..b25daac 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,16 +3,15 @@ use std::collections::HashMap; use gpui::prelude::*; use crate::{ - components::modal::ModalState, keymap::{Command, CommandDispatcher, ContextId, FocusTarget, KeyChord, KeymapStack}, models::{FilterState, ProjectTree}, - task::{self, TaskDetailState, TaskOverview, TaskService, TaskSummary}, + task::{self, TaskOverview, TaskService, TaskSummary}, theme::ActiveTheme, view::{ app_layout, sidebar::{Sidebar, SidebarEvent, SidebarSection, TagItem}, status_bar::{StatusBar, StatusBarEvent, SyncState}, - task_detail_modal, + task_detail_modal::{TaskDetailModal, TaskDetailModalEvent}, task_table::{TaskTable, TaskTableEvent}, }, }; @@ -25,14 +24,10 @@ pub(super) struct App { pub(super) filter_state: gpui::Entity, pub(super) status_bar: gpui::Entity, pub(super) task_table: gpui::Entity, + pub(super) task_detail_modal: gpui::Entity, pub(super) task_service: TaskService, pub(super) tasks: Vec, - pub(super) selected_task_id: Option, - pub(super) task_detail_state: TaskDetailState, - pub(super) modal_state: ModalState, - pub(super) modal_focus_handle: gpui::FocusHandle, pub(super) focus_before_modal: FocusTarget, - pub(super) modal_scroll_handle: gpui::ScrollHandle, } impl gpui::Render for App { @@ -63,22 +58,8 @@ impl gpui::Render for App { } }); - let modal = if self.modal_state.open { - let on_close_backdrop = - cx.listener(|app, _event: &gpui::MouseDownEvent, window, cx| { - app.close_task_detail(Some(window), cx); - }); - let on_close_click = cx.listener(|app, _event: &gpui::MouseDownEvent, window, cx| { - app.close_task_detail(Some(window), cx); - }); - Some(task_detail_modal::render_task_detail_modal( - &self.task_detail_state, - &self.modal_focus_handle, - &self.modal_scroll_handle, - theme, - on_close_backdrop, - on_close_click, - )) + let modal = if self.task_detail_modal.read(cx).is_open() { + Some(self.task_detail_modal.clone().into_any_element()) } else { None }; @@ -207,7 +188,9 @@ impl App { let context = self.active_context(cx); if let Some(command) = self.keymap.resolve(context, &chord) { - if self.modal_state.open { + let modal_is_open = self.task_detail_modal.read(cx).is_open(); + + if modal_is_open { match command { Command::CloseModal | Command::SaveModal @@ -295,7 +278,7 @@ impl App { window: Option<&mut gpui::Window>, cx: &mut gpui::Context, ) { - if self.modal_state.open { + if self.task_detail_modal.read(cx).is_open() { return; } @@ -314,76 +297,26 @@ impl App { cx: &mut gpui::Context, ) { self.focus_before_modal = self.focus_target; - self.modal_state.open = true; - if let Some(window) = window { - window.focus(&self.modal_focus_handle); - } - self.modal_scroll_handle = gpui::ScrollHandle::new(); - self.modal_scroll_handle.scroll_to_item(0); - - if self.selected_task_id == Some(task_id) { - if matches!(self.task_detail_state, TaskDetailState::Ready(_)) { - cx.notify(); - return; - } - } - - self.selected_task_id = Some(task_id); - self.task_detail_state = TaskDetailState::Loading(task_id); - cx.notify(); let tasks = self.tasks.clone(); match self.task_service.get_task_detail(task_id, &tasks) { Ok(detail) => { - self.task_detail_state = TaskDetailState::Ready(detail); + self.task_detail_modal.update(cx, |modal, cx| { + modal.open_with_detail(detail, window, cx); + }); } Err(e) => { - self.task_detail_state = TaskDetailState::Error(task_id, e.to_string()); + self.task_detail_modal.update(cx, |modal, cx| { + modal.open_with_error(task_id, e.to_string(), window, cx); + }); } } cx.notify(); } - pub(super) fn close_task_detail( - &mut self, - window: Option<&mut gpui::Window>, - cx: &mut gpui::Context, - ) { - if !self.modal_state.open { - return; - } - - self.modal_state.open = false; - self.selected_task_id = None; - self.focus_target = self.focus_before_modal; - - if let Some(window) = window { - window.focus(&self.focus_handle); - } - - cx.notify(); - } - - pub(super) fn scroll_task_detail(&self, delta: i32, cx: &mut gpui::Context) { - let handle = &self.modal_scroll_handle; - let current = if delta > 0 { - handle.bottom_item() - } else { - handle.top_item() - }; - let next = if delta > 0 { - current.saturating_add(1) - } else { - current.saturating_sub(1) - }; - - handle.scroll_to_item(next); - cx.notify(); - } - fn active_context(&self, cx: &gpui::Context) -> ContextId { - if self.modal_state.open { + if self.task_detail_modal.read(cx).is_open() { return ContextId::Modal; } if matches!(self.focus_target, FocusTarget::Table) { @@ -440,8 +373,12 @@ impl App { let task_table = cx .new(|cx| TaskTable::new("main-task-table", filter_state.clone(), cx)); + + let task_detail_modal = cx.new(|cx| TaskDetailModal::new(cx)); + let task_table_events = task_table.clone(); let sidebar_events = sidebar.clone(); + let modal_events = task_detail_modal.clone(); task_table.update(cx, |table, cx| { table.reload_tasks_from_all(task_summaries.clone(), cx); @@ -450,7 +387,7 @@ impl App { let mut keymap = KeymapStack::new(); keymap.push_layer(crate::keymap::defaults::build_default_keymap()); - let app = App { + let app_instance = App { focus_handle: cx.focus_handle(), focus_target: FocusTarget::Table, keymap, @@ -458,17 +395,13 @@ impl App { filter_state: filter_state.clone(), status_bar: status_bar.clone(), task_table, + task_detail_modal, task_service, tasks: task_summaries, - selected_task_id: None, - task_detail_state: TaskDetailState::default(), - modal_state: ModalState::default(), - modal_focus_handle: cx.focus_handle(), focus_before_modal: FocusTarget::Table, - modal_scroll_handle: gpui::ScrollHandle::new(), }; - window.focus(&app.focus_handle); + window.focus(&app_instance.focus_handle); cx.observe(&filter_state, |app, _, cx| { app.reload_tasks(cx); @@ -481,6 +414,7 @@ impl App { } }) .detach(); + cx.subscribe(&sidebar_events, |app, _sidebar, event, cx| match event { SidebarEvent::Focused(section) => { app.focus_target = match section { @@ -491,16 +425,25 @@ impl App { } }) .detach(); + cx.subscribe(&task_table_events, |app, _table, event, cx| match event { TaskTableEvent::OpenTask(task_id) => { - if !app.modal_state.open { + if !app.task_detail_modal.read(cx).is_open() { app.open_task_detail(*task_id, None, cx); } } }) .detach(); - app + cx.subscribe(&modal_events, |app, _modal, event, cx| match event { + TaskDetailModalEvent::Closed => { + app.focus_target = app.focus_before_modal; + cx.notify(); + } + }) + .detach(); + + app_instance }) }, ) diff --git a/src/dispatcher.rs b/src/dispatcher.rs index 99e4742..c51d016 100644 --- a/src/dispatcher.rs +++ b/src/dispatcher.rs @@ -3,6 +3,20 @@ use crate::{ keymap::{Command, CommandDispatcher, FocusTarget}, }; +impl App { + fn close_task_detail(&mut self, cx: &mut gpui::Context) { + self.task_detail_modal.update(cx, |modal, cx| { + modal.close(cx); + }); + } + + fn scroll_task_detail(&self, delta: i32, cx: &mut gpui::Context) { + self.task_detail_modal.update(cx, |modal, cx| { + modal.scroll(delta, cx); + }); + } +} + impl CommandDispatcher for App { fn dispatch(&mut self, command: Command, cx: &mut gpui::Context) -> bool { match command { @@ -94,11 +108,11 @@ impl CommandDispatcher for App { true } Command::CloseModal => { - self.close_task_detail(None, cx); + self.close_task_detail(cx); true } Command::SaveModal => { - self.close_task_detail(None, cx); + self.close_task_detail(cx); true } Command::ModalScrollUp => { diff --git a/src/view/task_detail_modal.rs b/src/view/task_detail_modal.rs index 0b7c6b2..3214839 100644 --- a/src/view/task_detail_modal.rs +++ b/src/view/task_detail_modal.rs @@ -4,11 +4,163 @@ use std::sync::Arc; use crate::components::label::Label; use crate::components::modal::ModalFrame; use crate::task::model::TaskLinkVm; -use crate::task::{self, TaskDetailState}; -use crate::theme::Theme; +use crate::task::{self, TaskDetailState, TaskDetailVm}; +use crate::theme::{ActiveTheme, Theme}; use crate::ui::{DATE_FORMAT, DATE_TIME_FORMAT}; -pub fn render_task_detail_modal( +pub enum TaskDetailModalEvent { + Closed, +} + +pub struct TaskDetailModal { + state: TaskDetailState, + is_open: bool, + focus_handle: gpui::FocusHandle, + scroll_handle: gpui::ScrollHandle, +} + +impl TaskDetailModal { + pub fn new(cx: &mut gpui::Context) -> Self { + Self { + state: TaskDetailState::default(), + is_open: false, + focus_handle: cx.focus_handle(), + scroll_handle: gpui::ScrollHandle::new(), + } + } + + pub fn is_open(&self) -> bool { + self.is_open + } + + pub fn focus_handle(&self) -> &gpui::FocusHandle { + &self.focus_handle + } + + pub fn open_with_detail( + &mut self, + detail: TaskDetailVm, + window: Option<&mut gpui::Window>, + cx: &mut gpui::Context, + ) { + if let Some(window) = window { + window.focus(&self.focus_handle); + } + + self.is_open = true; + self.scroll_handle = gpui::ScrollHandle::new(); + self.scroll_handle.scroll_to_item(0); + self.state = TaskDetailState::Ready(detail); + cx.notify(); + } + + pub fn open_with_error( + &mut self, + task_id: uuid::Uuid, + error: String, + window: Option<&mut gpui::Window>, + cx: &mut gpui::Context, + ) { + if let Some(window) = window { + window.focus(&self.focus_handle); + } + + self.is_open = true; + self.scroll_handle = gpui::ScrollHandle::new(); + self.scroll_handle.scroll_to_item(0); + self.state = TaskDetailState::Error(task_id, error); + cx.notify(); + } + + pub fn open_loading( + &mut self, + task_id: uuid::Uuid, + window: Option<&mut gpui::Window>, + cx: &mut gpui::Context, + ) { + if let Some(window) = window { + window.focus(&self.focus_handle); + } + + self.is_open = true; + self.scroll_handle = gpui::ScrollHandle::new(); + self.scroll_handle.scroll_to_item(0); + self.state = TaskDetailState::Loading(task_id); + cx.notify(); + } + + pub fn set_detail(&mut self, detail: TaskDetailVm, cx: &mut gpui::Context) { + self.state = TaskDetailState::Ready(detail); + cx.notify(); + } + + pub fn set_error(&mut self, task_id: uuid::Uuid, error: String, cx: &mut gpui::Context) { + self.state = TaskDetailState::Error(task_id, error); + cx.notify(); + } + + pub fn close(&mut self, cx: &mut gpui::Context) { + if !self.is_open { + return; + } + + self.is_open = false; + self.state = TaskDetailState::Idle; + cx.emit(TaskDetailModalEvent::Closed); + cx.notify(); + } + + pub fn scroll(&self, delta: i32, cx: &mut gpui::Context) { + let handle = &self.scroll_handle; + let current = if delta > 0 { + handle.bottom_item() + } else { + handle.top_item() + }; + let next = if delta > 0 { + current.saturating_add(1) + } else { + current.saturating_sub(1) + }; + + handle.scroll_to_item(next); + cx.notify(); + } +} + +impl gpui::EventEmitter for TaskDetailModal {} + +impl gpui::Render for TaskDetailModal { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + if !self.is_open { + return gpui::div().into_any_element(); + } + + let theme = cx.theme(); + + let on_close_backdrop = cx.listener(|modal, _event: &gpui::MouseDownEvent, _window, cx| { + modal.close(cx); + }); + let on_close_click = cx.listener(|modal, _event: &gpui::MouseDownEvent, _window, cx| { + modal.close(cx); + }); + + render_task_detail_modal( + &self.state, + &self.focus_handle, + &self.scroll_handle, + theme, + on_close_backdrop, + on_close_click, + ) + } +} + +fn render_task_detail_modal( detail_state: &TaskDetailState, focus_handle: &gpui::FocusHandle, scroll_handle: &gpui::ScrollHandle,