diff --git a/src/app.rs b/src/app.rs index b25daac..db6f168 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use gpui::prelude::*; use crate::{ + components::toast::{ToastGlobal, ToastHost}, keymap::{Command, CommandDispatcher, ContextId, FocusTarget, KeyChord, KeymapStack}, models::{FilterState, ProjectTree}, task::{self, TaskOverview, TaskService, TaskSummary}, @@ -25,6 +26,7 @@ pub(super) struct App { pub(super) status_bar: gpui::Entity, pub(super) task_table: gpui::Entity, pub(super) task_detail_modal: gpui::Entity, + pub(super) toast_host: gpui::Entity, pub(super) task_service: TaskService, pub(super) tasks: Vec, pub(super) focus_before_modal: FocusTarget, @@ -71,6 +73,7 @@ impl gpui::Render for App { self.sidebar.clone(), self.task_table.clone(), self.status_bar.clone(), + self.toast_host.clone(), on_root_key_down, on_sidebar_mouse_down, on_table_mouse_down, @@ -367,6 +370,10 @@ impl App { .collect(); let status_bar = cx.new(|cx| StatusBar::new(cx)); + let toast_host = cx.new(|cx| ToastHost::new(cx)); + cx.set_global(ToastGlobal { + host: toast_host.clone(), + }); let sidebar = cx.new(|cx| Sidebar::new(project_tree, tags, filter_state.clone(), cx)); @@ -396,6 +403,7 @@ impl App { status_bar: status_bar.clone(), task_table, task_detail_modal, + toast_host, task_service, tasks: task_summaries, focus_before_modal: FocusTarget::Table, diff --git a/src/components/mod.rs b/src/components/mod.rs index 8dbf874..30cab54 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -6,3 +6,4 @@ pub mod label; pub mod list; pub mod modal; pub mod panel; +pub mod toast; diff --git a/src/components/modal.rs b/src/components/modal.rs index ee063b2..b18b1c8 100644 --- a/src/components/modal.rs +++ b/src/components/modal.rs @@ -48,13 +48,7 @@ impl ModalFrame { impl gpui::RenderOnce for ModalFrame { fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl gpui::IntoElement { - let mut panel_wrap = gpui::div().child(self.panel); - - if let Some(handler) = self.on_close { - panel_wrap = panel_wrap.on_mouse_down_out(move |event, window, app| { - (handler)(event, window, app); - }); - } + let panel_wrap = gpui::div().child(self.panel); let root = gpui::div() .id(self.id) @@ -65,14 +59,20 @@ impl gpui::RenderOnce for ModalFrame { .occlude() .track_focus(&self.focus); - root.child( - gpui::div() - .size_full() - .bg(self.backdrop) - .absolute() - .top_0() - .left_0(), - ) + let mut backdrop = gpui::div() + .size_full() + .bg(self.backdrop) + .absolute() + .top_0() + .left_0(); + + if let Some(handler) = self.on_close { + backdrop = backdrop.on_mouse_down(gpui::MouseButton::Left, move |event, window, app| { + (handler)(event, window, app); + }); + } + + root.child(backdrop) .child( gpui::div() .size_full() diff --git a/src/components/toast.rs b/src/components/toast.rs new file mode 100644 index 0000000..2786a18 --- /dev/null +++ b/src/components/toast.rs @@ -0,0 +1,144 @@ +use gpui::prelude::*; + +use crate::components::label::Label; +use crate::theme::{ActiveTheme, Theme}; +use crate::ui::mix_color; + +pub struct ToastGlobal { + pub host: gpui::Entity, +} + +impl gpui::Global for ToastGlobal {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToastKind { + Info, + Success, + Error, +} + +struct Toast { + id: u64, + kind: ToastKind, + message: String, +} + +pub struct ToastHost { + toasts: Vec, + next_id: u64, +} + +impl ToastHost { + pub fn new(_cx: &mut gpui::Context) -> Self { + Self { + toasts: Vec::new(), + next_id: 1, + } + } + + pub fn push(&mut self, kind: ToastKind, message: impl Into, cx: &mut Context) { + let toast = Toast { + id: self.next_id, + kind, + message: message.into(), + }; + + self.next_id += 1; + self.toasts.push(toast); + + cx.notify(); + } + + fn dismiss(&mut self, id: u64, cx: &mut Context) { + self.toasts.retain(|toast| toast.id != id); + cx.notify(); + } + + fn kind_color(kind: ToastKind, theme: &Theme) -> gpui::Rgba { + match kind { + ToastKind::Info => theme.info, + ToastKind::Success => theme.success, + ToastKind::Error => theme.error, + } + } +} + +impl gpui::Render for ToastHost { + fn render( + &mut self, + _window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + if self.toasts.is_empty() { + return gpui::div().into_any_element(); + } + + let theme = cx.theme(); + let items = self.toasts.iter().map(|toast| { + let toast_id = toast.id; + let accent = Self::kind_color(toast.kind, theme); + + let close_button = gpui::div() + .flex() + .items_center() + .justify_center() + .w(gpui::rems(1.5)) + .h(gpui::rems(1.5)) + .text_sm() + .text_color(theme.muted) + .cursor_pointer() + .hover(|s| s.text_color(theme.accent)) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(move |host, _event, _window, cx| { + host.dismiss(toast_id, cx); + }), + ) + .child(Label::new("X")); + + let background = mix_color(theme.background, accent, 0.2); + let border = Theme::alpha(accent, 0.45); + + gpui::div() + .flex() + .items_center() + .gap_3() + .px_5() + .py_3() + .occlude() + .border_1() + .border_color(border) + .bg(background) + .rounded_md() + .shadow_lg() + .child( + gpui::div() + .w(gpui::px(6.0)) + .h_full() + .bg(Theme::alpha(accent, 0.9)) + .rounded_md(), + ) + .child( + gpui::div().flex_1().min_w(gpui::rems(18.0)).child( + Label::new(toast.message.clone()) + .text_sm() + .text_color(theme.foreground) + .font_weight(gpui::FontWeight::MEDIUM), + ), + ) + .child(close_button) + .into_any_element() + }); + + gpui::div() + .id("toast-host") + .absolute() + .top(gpui::rems(1.0)) + .right(gpui::rems(1.0)) + .flex() + .flex_col() + .gap_2() + .children(items) + .into_any_element() + } +} diff --git a/src/ui.rs b/src/ui.rs index 67d9366..322f468 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,7 @@ use gpui::prelude::*; use gpui::{Pixels, px, rems}; -use crate::theme::Theme; +use crate::theme::{Color, Theme}; pub const CARD_RADIUS: Pixels = px(6.0); pub const CARD_PADDING: Pixels = px(8.0); @@ -170,3 +170,13 @@ pub fn text_button_style(div: gpui::Div, theme: &Theme) -> gpui::Div { .cursor_pointer() .hover(|s| s.text_color(theme.accent)) } + +pub fn mix_color(base: Color, tint: Color, amount: f32) -> Color { + let amount = amount.clamp(0.0, 1.0); + gpui::Rgba { + r: base.r + (tint.r - base.r) * amount, + g: base.g + (tint.g - base.g) * amount, + b: base.b + (tint.b - base.b) * amount, + a: 1.0, + } +} diff --git a/src/view/app_layout.rs b/src/view/app_layout.rs index 0acb999..a7cc4f1 100644 --- a/src/view/app_layout.rs +++ b/src/view/app_layout.rs @@ -1,5 +1,6 @@ use gpui::prelude::*; +use crate::components::toast::ToastHost; use crate::keymap::FocusTarget; use crate::theme::Theme; use crate::ui::{CARD_PADDING, CARD_RADIUS, ROOT_PADDING, SECTION_GAP, SIDEBAR_WIDTH}; @@ -14,6 +15,7 @@ pub fn render_app_layout( sidebar: gpui::Entity, task_table: gpui::Entity, status_bar: gpui::Entity, + toast_host: gpui::Entity, on_root_key_down: impl Fn(&gpui::KeyDownEvent, &mut gpui::Window, &mut gpui::App) + 'static, on_sidebar_mouse_down: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut gpui::App) + 'static, on_table_mouse_down: impl Fn(&gpui::MouseDownEvent, &mut gpui::Window, &mut gpui::App) + 'static, @@ -87,5 +89,15 @@ pub fn render_app_layout( root = root.child(modal); } + let toast_layer = gpui::div() + .absolute() + .top_0() + .left_0() + .right_0() + .bottom_0() + .child(toast_host); + + root = root.child(toast_layer); + root.into_any_element() } diff --git a/src/view/task_detail_modal.rs b/src/view/task_detail_modal.rs index 74f8da6..4713468 100644 --- a/src/view/task_detail_modal.rs +++ b/src/view/task_detail_modal.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::components::label::Label; use crate::components::modal::ModalFrame; +use crate::components::toast::{ToastGlobal, ToastKind}; use crate::task::model::TaskLinkVm; use crate::task::{self, TaskDetailState, TaskDetailVm}; use crate::theme::{ActiveTheme, Theme}; @@ -632,7 +633,6 @@ where .map(|(index, annotation)| { let timestamp = annotation.entry.format(DATE_TIME_FORMAT).to_string(); let content_for_copy = annotation.content.clone(); - let copy_action = gpui::div() .text_xs() .text_color(theme.muted) @@ -642,6 +642,10 @@ where app.write_to_clipboard(gpui::ClipboardItem::new_string( content_for_copy.clone(), )); + let toast_host = app.global::().host.clone(); + app.update_entity(&toast_host, |host, cx| { + host.push(ToastKind::Info, "Annotation copied", cx); + }); }) .child(Label::new("Copy")); diff --git a/src/view/task_table.rs b/src/view/task_table.rs index 15b3d4f..2724c6d 100644 --- a/src/view/task_table.rs +++ b/src/view/task_table.rs @@ -592,17 +592,6 @@ impl TaskTable { } } - fn priority_color(&self, row: &TaskRow, cx: &gpui::Context) -> theme::Color { - let theme = cx.theme(); - - match row.priority.as_str() { - "High" => theme.high, - "Medium" => theme.medium, - "Low" => theme.low, - _ => theme.foreground, - } - } - fn due_color(&self, row: &TaskRow, cx: &gpui::Context) -> theme::Color { let theme = cx.theme();