feat: Add global toasts and annotation copy feedback
Create a reusable ToastHost/ToastGlobal rendered as an overlay, and use it to show a toast after copying an annotation. Adjust modal backdrop click handling so toasts don’t close the modal, add a simple color mix helper for toast styling, and remove the unused task table priority color helper.
This commit is contained in:
@@ -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<StatusBar>,
|
||||
pub(super) task_table: gpui::Entity<TaskTable>,
|
||||
pub(super) task_detail_modal: gpui::Entity<TaskDetailModal>,
|
||||
pub(super) toast_host: gpui::Entity<ToastHost>,
|
||||
pub(super) task_service: TaskService,
|
||||
pub(super) tasks: Vec<TaskSummary>,
|
||||
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,
|
||||
|
||||
@@ -6,3 +6,4 @@ pub mod label;
|
||||
pub mod list;
|
||||
pub mod modal;
|
||||
pub mod panel;
|
||||
pub mod toast;
|
||||
|
||||
+11
-11
@@ -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()
|
||||
let mut backdrop = gpui::div()
|
||||
.size_full()
|
||||
.bg(self.backdrop)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_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()
|
||||
|
||||
@@ -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<ToastHost>,
|
||||
}
|
||||
|
||||
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<Toast>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl ToastHost {
|
||||
pub fn new(_cx: &mut gpui::Context<Self>) -> Self {
|
||||
Self {
|
||||
toasts: Vec::new(),
|
||||
next_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, kind: ToastKind, message: impl Into<String>, cx: &mut Context<Self>) {
|
||||
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>) {
|
||||
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<Self>,
|
||||
) -> 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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Sidebar>,
|
||||
task_table: gpui::Entity<TaskTable>,
|
||||
status_bar: gpui::Entity<StatusBar>,
|
||||
toast_host: gpui::Entity<ToastHost>,
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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::<ToastGlobal>().host.clone();
|
||||
app.update_entity(&toast_host, |host, cx| {
|
||||
host.push(ToastKind::Info, "Annotation copied", cx);
|
||||
});
|
||||
})
|
||||
.child(Label::new("Copy"));
|
||||
|
||||
|
||||
@@ -592,17 +592,6 @@ impl TaskTable {
|
||||
}
|
||||
}
|
||||
|
||||
fn priority_color(&self, row: &TaskRow, cx: &gpui::Context<Self>) -> 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<Self>) -> theme::Color {
|
||||
let theme = cx.theme();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user