Add date picker and task improvements
This commit is contained in:
Generated
+1
-1
@@ -6497,7 +6497,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "task-warrior-gpui"
|
name = "task-warrior-gpui"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
rustPlatform,
|
||||||
|
fetchFromGitHub,
|
||||||
|
pkg-config,
|
||||||
|
cmake,
|
||||||
|
fontconfig,
|
||||||
|
libxkbcommon,
|
||||||
|
wayland,
|
||||||
|
vulkan-loader,
|
||||||
|
libxcb,
|
||||||
|
}:
|
||||||
|
|
||||||
|
rustPlatform.buildRustPackage rec {
|
||||||
|
pname = "task-warrior-gpui";
|
||||||
|
version = "0.3.0";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "0xErwin1";
|
||||||
|
repo = "taskwarrior-gpui";
|
||||||
|
rev = "v${version}";
|
||||||
|
hash = "sha256-6nndsC5Q+4zmWFESpjas0c6BRb8medtLXoKzrPzpUus=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-VPKLkLmMbzcILT8w1DDbvwOpeOraz2vSRiztWfR49nc=";
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
cmake
|
||||||
|
rustPlatform.bindgenHook
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
fontconfig
|
||||||
|
libxkbcommon
|
||||||
|
wayland
|
||||||
|
libxcb
|
||||||
|
];
|
||||||
|
|
||||||
|
postFixup = ''
|
||||||
|
patchelf $out/bin/task-warrior-gpui --add-rpath ${
|
||||||
|
lib.makeLibraryPath [
|
||||||
|
wayland
|
||||||
|
vulkan-loader
|
||||||
|
]
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "A desktop GUI for TaskWarrior built with GPUI";
|
||||||
|
homepage = "https://github.com/0xErwin1/taskwarrior-gpui";
|
||||||
|
license = with lib.licenses; [
|
||||||
|
mit
|
||||||
|
asl20
|
||||||
|
];
|
||||||
|
mainProgram = "task-warrior-gpui";
|
||||||
|
platforms = lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.wayland
|
||||||
|
pkgs.libxkbcommon
|
||||||
|
pkgs.vulkan-loader
|
||||||
|
pkgs.fontconfig
|
||||||
|
];
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
||||||
|
pkgs.wayland
|
||||||
|
pkgs.libxkbcommon
|
||||||
|
pkgs.vulkan-loader
|
||||||
|
pkgs.fontconfig
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::{Datelike, NaiveDate, Utc};
|
||||||
|
use gpui::prelude::*;
|
||||||
|
use gpui::{Corner, anchored, deferred, point, px};
|
||||||
|
|
||||||
|
use crate::theme::ActiveTheme;
|
||||||
|
use crate::ui::{DATE_FORMAT, clickable_control_style};
|
||||||
|
|
||||||
|
pub struct DatePicker {
|
||||||
|
id: gpui::ElementId,
|
||||||
|
value: Option<NaiveDate>,
|
||||||
|
view_month: NaiveDate,
|
||||||
|
open: bool,
|
||||||
|
placeholder: gpui::SharedString,
|
||||||
|
on_select: Option<Arc<dyn Fn(Option<NaiveDate>, &mut gpui::Context<Self>) + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatePicker {
|
||||||
|
pub fn new(id: impl Into<gpui::ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
value: None,
|
||||||
|
view_month: Utc::now().date_naive().with_day(1).unwrap(),
|
||||||
|
open: false,
|
||||||
|
placeholder: "Select date".into(),
|
||||||
|
on_select: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_select(
|
||||||
|
mut self,
|
||||||
|
handler: Arc<dyn Fn(Option<NaiveDate>, &mut gpui::Context<Self>) + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
self.on_select = Some(handler);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open(&self) -> bool {
|
||||||
|
self.open
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.view_month = self
|
||||||
|
.value
|
||||||
|
.unwrap_or_else(|| Utc::now().date_naive())
|
||||||
|
.with_day(1)
|
||||||
|
.unwrap();
|
||||||
|
self.open = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_open(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
|
if self.open {
|
||||||
|
self.close(cx);
|
||||||
|
} else {
|
||||||
|
self.open(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&mut self, value: Option<NaiveDate>, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.value = value;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select(&mut self, date: Option<NaiveDate>, cx: &mut gpui::Context<Self>) {
|
||||||
|
self.value = date;
|
||||||
|
self.open = false;
|
||||||
|
if let Some(handler) = self.on_select.clone() {
|
||||||
|
handler(date, cx);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shift_month(&mut self, delta: i32, cx: &mut gpui::Context<Self>) {
|
||||||
|
let total = self.view_month.year() * 12 + (self.view_month.month() as i32 - 1) + delta;
|
||||||
|
let year = total.div_euclid(12);
|
||||||
|
let month = total.rem_euclid(12) + 1;
|
||||||
|
if let Some(date) = NaiveDate::from_ymd_opt(year, month as u32, 1) {
|
||||||
|
self.view_month = date;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mouse_down_out(
|
||||||
|
&mut self,
|
||||||
|
_event: &gpui::MouseDownEvent,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.open {
|
||||||
|
self.open = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_calendar(&self, cx: &gpui::Context<Self>) -> gpui::AnyElement {
|
||||||
|
if !self.open {
|
||||||
|
return gpui::div().into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme = cx.theme();
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
let month_label = self.view_month.format("%B %Y").to_string();
|
||||||
|
|
||||||
|
let nav_button = |id: &'static str, label: &'static str, delta: i32| {
|
||||||
|
gpui::div()
|
||||||
|
.id(id)
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_md()
|
||||||
|
.cursor_pointer()
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.hover(|s| s.bg(theme.hover).text_color(theme.foreground))
|
||||||
|
.on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(move |this, _, _, cx| this.shift_month(delta, cx)),
|
||||||
|
)
|
||||||
|
.child(label)
|
||||||
|
};
|
||||||
|
|
||||||
|
let header = gpui::div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.pb_1()
|
||||||
|
.child(nav_button("date-picker-prev", "‹", -1))
|
||||||
|
.child(
|
||||||
|
gpui::div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
.child(month_label),
|
||||||
|
)
|
||||||
|
.child(nav_button("date-picker-next", "›", 1));
|
||||||
|
|
||||||
|
let weekday_row = gpui::div().flex().children(
|
||||||
|
["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| {
|
||||||
|
gpui::div()
|
||||||
|
.w(px(28.0))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.child(d)
|
||||||
|
.into_any_element()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let leading = self.view_month.weekday().num_days_from_monday() as i64;
|
||||||
|
let grid_start = self.view_month - chrono::Duration::days(leading);
|
||||||
|
|
||||||
|
let mut weeks = Vec::with_capacity(6);
|
||||||
|
for week in 0..6i64 {
|
||||||
|
let mut row = gpui::div().flex();
|
||||||
|
for day in 0..7i64 {
|
||||||
|
let date = grid_start + chrono::Duration::days(week * 7 + day);
|
||||||
|
let in_month = date.month() == self.view_month.month()
|
||||||
|
&& date.year() == self.view_month.year();
|
||||||
|
let is_today = date == today;
|
||||||
|
let is_selected = self.value == Some(date);
|
||||||
|
|
||||||
|
let mut cell = gpui::div()
|
||||||
|
.id(("date-picker-day", date.num_days_from_ce() as u64))
|
||||||
|
.w(px(28.0))
|
||||||
|
.h(px(28.0))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.rounded_md()
|
||||||
|
.text_sm()
|
||||||
|
.cursor_pointer();
|
||||||
|
|
||||||
|
cell = if is_selected {
|
||||||
|
cell.bg(theme.accent).text_color(theme.selection_foreground)
|
||||||
|
} else if is_today {
|
||||||
|
cell.border_1()
|
||||||
|
.border_color(theme.accent)
|
||||||
|
.text_color(theme.foreground)
|
||||||
|
} else if in_month {
|
||||||
|
cell.text_color(theme.foreground).hover(|s| s.bg(theme.hover))
|
||||||
|
} else {
|
||||||
|
cell.text_color(theme.muted).hover(|s| s.bg(theme.hover))
|
||||||
|
};
|
||||||
|
|
||||||
|
cell = cell.on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(move |this, _, _, cx| {
|
||||||
|
this.select(Some(date), cx);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
row = row.child(cell.child(date.day().to_string()));
|
||||||
|
}
|
||||||
|
weeks.push(row.into_any_element());
|
||||||
|
}
|
||||||
|
|
||||||
|
let today_button = gpui::div()
|
||||||
|
.id("date-picker-today")
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_md()
|
||||||
|
.text_xs()
|
||||||
|
.cursor_pointer()
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.hover(|s| s.text_color(theme.accent))
|
||||||
|
.on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(|this, _, _, cx| {
|
||||||
|
let today = Utc::now().date_naive();
|
||||||
|
this.select(Some(today), cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child("Today");
|
||||||
|
|
||||||
|
let clear_button = gpui::div()
|
||||||
|
.id("date-picker-clear")
|
||||||
|
.px_2()
|
||||||
|
.py_1()
|
||||||
|
.rounded_md()
|
||||||
|
.text_xs()
|
||||||
|
.cursor_pointer()
|
||||||
|
.text_color(theme.muted)
|
||||||
|
.hover(|s| s.text_color(theme.error))
|
||||||
|
.on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(|this, _, _, cx| {
|
||||||
|
this.select(None, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child("Clear");
|
||||||
|
|
||||||
|
let footer = gpui::div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.pt_1()
|
||||||
|
.mt_1()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(theme.divider)
|
||||||
|
.child(today_button)
|
||||||
|
.child(clear_button);
|
||||||
|
|
||||||
|
let popup = gpui::div()
|
||||||
|
.w(px(224.0))
|
||||||
|
.p_2()
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border)
|
||||||
|
.bg(theme.background)
|
||||||
|
.rounded_md()
|
||||||
|
.overflow_hidden()
|
||||||
|
.shadow_lg()
|
||||||
|
.occlude()
|
||||||
|
.child(header)
|
||||||
|
.child(weekday_row)
|
||||||
|
.children(weeks)
|
||||||
|
.child(footer);
|
||||||
|
|
||||||
|
deferred(
|
||||||
|
anchored()
|
||||||
|
.anchor(Corner::TopLeft)
|
||||||
|
.offset(point(px(0.0), px(4.0)))
|
||||||
|
.snap_to_window()
|
||||||
|
.child(popup),
|
||||||
|
)
|
||||||
|
.with_priority(1)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Render for DatePicker {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
let label = self
|
||||||
|
.value
|
||||||
|
.map(|d| d.format(DATE_FORMAT).to_string())
|
||||||
|
.unwrap_or_else(|| self.placeholder.to_string());
|
||||||
|
|
||||||
|
let trigger = clickable_control_style(
|
||||||
|
gpui::div()
|
||||||
|
.child(label)
|
||||||
|
.child(gpui::div().text_color(theme.muted).child("▾")),
|
||||||
|
theme,
|
||||||
|
);
|
||||||
|
|
||||||
|
let trigger_wrap = gpui::div().flex_shrink_0().child(trigger).on_mouse_down(
|
||||||
|
gpui::MouseButton::Left,
|
||||||
|
cx.listener(Self::handle_trigger_mouse_down),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut container = gpui::div()
|
||||||
|
.id(self.id.clone())
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.child(trigger_wrap)
|
||||||
|
.child(self.render_calendar(cx));
|
||||||
|
|
||||||
|
if self.open {
|
||||||
|
container = container.on_mouse_down_out(cx.listener(Self::handle_mouse_down_out));
|
||||||
|
}
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatePicker {
|
||||||
|
fn handle_trigger_mouse_down(
|
||||||
|
&mut self,
|
||||||
|
_event: &gpui::MouseDownEvent,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::Context<Self>,
|
||||||
|
) {
|
||||||
|
self.toggle_open(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -276,19 +276,15 @@ impl Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
fn refresh_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
if let Some(suggest) = &self.suggest {
|
self.open_suggestions(cx);
|
||||||
self.suggestions = suggest(&self.value);
|
|
||||||
self.active_suggestion = 0;
|
|
||||||
self.suggestions_open = !self.suggestions.is_empty() && !self.value.is_empty();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
/// Recomputes and shows suggestions for the current value; public so a field can browse all options on focus.
|
||||||
|
pub fn open_suggestions(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
if let Some(suggest) = &self.suggest {
|
if let Some(suggest) = &self.suggest {
|
||||||
self.suggestions = suggest(&self.value);
|
self.suggestions = suggest(&self.value);
|
||||||
self.active_suggestion = 0;
|
self.active_suggestion = 0;
|
||||||
self.suggestions_open = !self.suggestions.is_empty() && !self.value.is_empty();
|
self.suggestions_open = !self.suggestions.is_empty();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod action_button;
|
|||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod chip;
|
pub mod chip;
|
||||||
pub mod confirm_dialog;
|
pub mod confirm_dialog;
|
||||||
|
pub mod date_picker;
|
||||||
pub mod dialog;
|
pub mod dialog;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod field_row;
|
pub mod field_row;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ impl FocusMap {
|
|||||||
focus,
|
focus,
|
||||||
ModalFocus::Description
|
ModalFocus::Description
|
||||||
| ModalFocus::Project
|
| ModalFocus::Project
|
||||||
| ModalFocus::Due
|
|
||||||
| ModalFocus::TagsInput
|
| ModalFocus::TagsInput
|
||||||
| ModalFocus::AnnotationsInput
|
| ModalFocus::AnnotationsInput
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::collections::HashSet;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use crate::components::button::{Dropdown, DropdownItem};
|
use crate::components::button::{Dropdown, DropdownItem};
|
||||||
|
use crate::components::date_picker::DatePicker;
|
||||||
use crate::components::input::Input;
|
use crate::components::input::Input;
|
||||||
use crate::components::toast::{ToastGlobal, ToastKind};
|
use crate::components::toast::{ToastGlobal, ToastKind};
|
||||||
use crate::keymap::{Command, CommandDispatcher, ContextId};
|
use crate::keymap::{Command, CommandDispatcher, ContextId};
|
||||||
@@ -45,7 +46,7 @@ struct ModalEntities {
|
|||||||
annotation_input: gpui::Entity<Input>,
|
annotation_input: gpui::Entity<Input>,
|
||||||
description_input: gpui::Entity<Input>,
|
description_input: gpui::Entity<Input>,
|
||||||
project_input: gpui::Entity<Input>,
|
project_input: gpui::Entity<Input>,
|
||||||
due_input: gpui::Entity<Input>,
|
due_picker: gpui::Entity<DatePicker>,
|
||||||
tags_input: gpui::Entity<Input>,
|
tags_input: gpui::Entity<Input>,
|
||||||
status_dropdown: gpui::Entity<Dropdown>,
|
status_dropdown: gpui::Entity<Dropdown>,
|
||||||
priority_dropdown: gpui::Entity<Dropdown>,
|
priority_dropdown: gpui::Entity<Dropdown>,
|
||||||
@@ -102,15 +103,11 @@ impl TaskDetailModal {
|
|||||||
Input::new("task-edit-project", cx, "Project").with_suggest(Arc::new(
|
Input::new("task-edit-project", cx, "Project").with_suggest(Arc::new(
|
||||||
move |query| {
|
move |query| {
|
||||||
let query = query.trim();
|
let query = query.trim();
|
||||||
if query.is_empty() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(list) = suggestions.lock() else {
|
let Ok(list) = suggestions.lock() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let needle = query.to_lowercase();
|
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
@@ -146,6 +143,17 @@ impl TaskDetailModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty query: show every known project, like a dropdown menu.
|
||||||
|
if query.is_empty() {
|
||||||
|
candidates.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
|
||||||
|
return candidates
|
||||||
|
.into_iter()
|
||||||
|
.take(20)
|
||||||
|
.map(crate::components::input::Suggestion::simple)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let needle = query.to_lowercase();
|
||||||
let mut level_matches = Vec::new();
|
let mut level_matches = Vec::new();
|
||||||
let mut prefix_matches = Vec::new();
|
let mut prefix_matches = Vec::new();
|
||||||
let mut contains_matches = Vec::new();
|
let mut contains_matches = Vec::new();
|
||||||
@@ -182,7 +190,23 @@ impl TaskDetailModal {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let due_input = cx.new(|cx| Input::new("task-edit-due", cx, "Due (YYYY-MM-DD)"));
|
let due_picker = {
|
||||||
|
let modal_entity = modal_entity.clone();
|
||||||
|
cx.new(|_cx| {
|
||||||
|
DatePicker::new("task-edit-due").on_select(Arc::new(move |date, cx| {
|
||||||
|
let modal_entity = modal_entity.clone();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let _ = modal_entity.update(cx, |modal, cx| {
|
||||||
|
modal.state.form.due = date
|
||||||
|
.map(|d| d.format(DATE_FORMAT).to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
modal.update_field_error(FieldId::Due);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let tags_input = {
|
let tags_input = {
|
||||||
let on_change_entity = modal_entity.clone();
|
let on_change_entity = modal_entity.clone();
|
||||||
@@ -313,7 +337,7 @@ impl TaskDetailModal {
|
|||||||
annotation_input,
|
annotation_input,
|
||||||
description_input,
|
description_input,
|
||||||
project_input,
|
project_input,
|
||||||
due_input,
|
due_picker,
|
||||||
tags_input,
|
tags_input,
|
||||||
status_dropdown,
|
status_dropdown,
|
||||||
priority_dropdown,
|
priority_dropdown,
|
||||||
@@ -638,8 +662,9 @@ impl TaskDetailModal {
|
|||||||
input.set_value_silent(form.project, cx);
|
input.set_value_silent(form.project, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.entities.due_input.update(cx, |input, cx| {
|
let due_date = NaiveDate::parse_from_str(form.due.trim(), DATE_FORMAT).ok();
|
||||||
input.set_value_silent(form.due, cx);
|
self.entities.due_picker.update(cx, |picker, cx| {
|
||||||
|
picker.set_value(due_date, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.entities.tags_input.update(cx, |input, cx| {
|
self.entities.tags_input.update(cx, |input, cx| {
|
||||||
@@ -685,12 +710,6 @@ impl TaskDetailModal {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_edit_due(&mut self, value: &str, cx: &mut gpui::Context<Self>) {
|
|
||||||
self.state.form.due = value.to_string();
|
|
||||||
self.update_field_error(FieldId::Due);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_edit_tags(&mut self, value: &str, cx: &mut gpui::Context<Self>) {
|
fn update_edit_tags(&mut self, value: &str, cx: &mut gpui::Context<Self>) {
|
||||||
self.state.form.tag_draft = value.to_string();
|
self.state.form.tag_draft = value.to_string();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -721,7 +740,6 @@ impl TaskDetailModal {
|
|||||||
fn sync_form_from_inputs(&mut self, cx: &gpui::Context<Self>) {
|
fn sync_form_from_inputs(&mut self, cx: &gpui::Context<Self>) {
|
||||||
self.state.form.description = self.entities.description_input.read(cx).value().to_string();
|
self.state.form.description = self.entities.description_input.read(cx).value().to_string();
|
||||||
self.state.form.project = self.entities.project_input.read(cx).value().to_string();
|
self.state.form.project = self.entities.project_input.read(cx).value().to_string();
|
||||||
self.state.form.due = self.entities.due_input.read(cx).value().to_string();
|
|
||||||
self.state.form.tag_draft = self.entities.tags_input.read(cx).value().to_string();
|
self.state.form.tag_draft = self.entities.tags_input.read(cx).value().to_string();
|
||||||
self.state.annotations.draft = self
|
self.state.annotations.draft = self
|
||||||
.entities
|
.entities
|
||||||
@@ -903,8 +921,8 @@ impl TaskDetailModal {
|
|||||||
.project_input
|
.project_input
|
||||||
.update(cx, |input, cx| input.blur(window, cx));
|
.update(cx, |input, cx| input.blur(window, cx));
|
||||||
self.entities
|
self.entities
|
||||||
.due_input
|
.due_picker
|
||||||
.update(cx, |input, cx| input.blur(window, cx));
|
.update(cx, |picker, cx| picker.close(cx));
|
||||||
self.entities
|
self.entities
|
||||||
.tags_input
|
.tags_input
|
||||||
.update(cx, |input, cx| input.blur(window, cx));
|
.update(cx, |input, cx| input.blur(window, cx));
|
||||||
@@ -989,11 +1007,6 @@ impl TaskDetailModal {
|
|||||||
.project_input
|
.project_input
|
||||||
.update(cx, |i, cx| i.focus(window, cx));
|
.update(cx, |i, cx| i.focus(window, cx));
|
||||||
}
|
}
|
||||||
ModalFocus::Due => {
|
|
||||||
self.entities
|
|
||||||
.due_input
|
|
||||||
.update(cx, |i, cx| i.focus(window, cx));
|
|
||||||
}
|
|
||||||
ModalFocus::TagsInput => {
|
ModalFocus::TagsInput => {
|
||||||
self.entities
|
self.entities
|
||||||
.tags_input
|
.tags_input
|
||||||
@@ -1004,13 +1017,16 @@ impl TaskDetailModal {
|
|||||||
.annotation_input
|
.annotation_input
|
||||||
.update(cx, |i, cx| i.focus(window, cx));
|
.update(cx, |i, cx| i.focus(window, cx));
|
||||||
}
|
}
|
||||||
ModalFocus::None | ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {}
|
ModalFocus::None
|
||||||
|
| ModalFocus::StatusDropdown
|
||||||
|
| ModalFocus::PriorityDropdown
|
||||||
|
| ModalFocus::Due => {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match focus {
|
match focus {
|
||||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {
|
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due => {
|
||||||
window.focus(&self.form_focus_handle);
|
window.focus(&self.form_focus_handle);
|
||||||
}
|
}
|
||||||
ModalFocus::None => {
|
ModalFocus::None => {
|
||||||
@@ -1018,7 +1034,6 @@ impl TaskDetailModal {
|
|||||||
}
|
}
|
||||||
ModalFocus::Description
|
ModalFocus::Description
|
||||||
| ModalFocus::Project
|
| ModalFocus::Project
|
||||||
| ModalFocus::Due
|
|
||||||
| ModalFocus::TagsInput
|
| ModalFocus::TagsInput
|
||||||
| ModalFocus::AnnotationsInput => {}
|
| ModalFocus::AnnotationsInput => {}
|
||||||
}
|
}
|
||||||
@@ -1040,12 +1055,14 @@ impl TaskDetailModal {
|
|||||||
self.entities
|
self.entities
|
||||||
.priority_dropdown
|
.priority_dropdown
|
||||||
.update(cx, |d, cx| d.close(cx));
|
.update(cx, |d, cx| d.close(cx));
|
||||||
|
self.entities.due_picker.update(cx, |d, cx| d.close(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_open_dropdown(&self, cx: &gpui::Context<Self>) -> bool {
|
fn has_open_dropdown(&self, cx: &gpui::Context<Self>) -> bool {
|
||||||
let status_open = self.entities.status_dropdown.read(cx).is_open();
|
let status_open = self.entities.status_dropdown.read(cx).is_open();
|
||||||
let priority_open = self.entities.priority_dropdown.read(cx).is_open();
|
let priority_open = self.entities.priority_dropdown.read(cx).is_open();
|
||||||
status_open || priority_open
|
let due_open = self.entities.due_picker.read(cx).is_open();
|
||||||
|
status_open || priority_open || due_open
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_focused_dropdown(&mut self, cx: &mut gpui::Context<Self>) {
|
pub fn toggle_focused_dropdown(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
@@ -1090,11 +1107,22 @@ impl TaskDetailModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ModalFocus::Due => {
|
||||||
|
self.entities.due_picker.update(cx, |d, cx| {
|
||||||
|
if d.is_open() {
|
||||||
|
d.close(cx);
|
||||||
|
} else {
|
||||||
|
d.open(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ponytail: due picker is mouse-only (no arrow-key day navigation);
|
||||||
|
// add if keyboard-driven date entry is needed.
|
||||||
pub fn select_next_dropdown_option(&mut self, cx: &mut gpui::Context<Self>) {
|
pub fn select_next_dropdown_option(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
let select_next = |d: &gpui::Entity<Dropdown>, cx: &mut gpui::Context<Self>| {
|
let select_next = |d: &gpui::Entity<Dropdown>, cx: &mut gpui::Context<Self>| {
|
||||||
d.update(cx, |d, cx| {
|
d.update(cx, |d, cx| {
|
||||||
@@ -1802,7 +1830,7 @@ impl TaskDetailModal {
|
|||||||
if self.state.mode == ModalMode::Edit
|
if self.state.mode == ModalMode::Edit
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.state.modal_focus,
|
self.state.modal_focus,
|
||||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown
|
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if self.state.edit_state == EditState::DropdownOpen {
|
if self.state.edit_state == EditState::DropdownOpen {
|
||||||
@@ -1911,13 +1939,12 @@ impl TaskDetailModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.state.modal_focus {
|
match self.state.modal_focus {
|
||||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {
|
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due => {
|
||||||
self.state.edit_state = EditState::DropdownOpen;
|
self.state.edit_state = EditState::DropdownOpen;
|
||||||
self.toggle_focused_dropdown(cx);
|
self.toggle_focused_dropdown(cx);
|
||||||
}
|
}
|
||||||
ModalFocus::Description
|
ModalFocus::Description
|
||||||
| ModalFocus::Project
|
| ModalFocus::Project
|
||||||
| ModalFocus::Due
|
|
||||||
| ModalFocus::TagsInput
|
| ModalFocus::TagsInput
|
||||||
| ModalFocus::AnnotationsInput => {
|
| ModalFocus::AnnotationsInput => {
|
||||||
self.state.edit_state = EditState::Editing;
|
self.state.edit_state = EditState::Editing;
|
||||||
@@ -1930,14 +1957,10 @@ impl TaskDetailModal {
|
|||||||
.update(cx, |i, cx| i.focus(window, cx));
|
.update(cx, |i, cx| i.focus(window, cx));
|
||||||
}
|
}
|
||||||
ModalFocus::Project => {
|
ModalFocus::Project => {
|
||||||
self.entities
|
self.entities.project_input.update(cx, |i, cx| {
|
||||||
.project_input
|
i.focus(window, cx);
|
||||||
.update(cx, |i, cx| i.focus(window, cx));
|
i.open_suggestions(cx);
|
||||||
}
|
});
|
||||||
ModalFocus::Due => {
|
|
||||||
self.entities
|
|
||||||
.due_input
|
|
||||||
.update(cx, |i, cx| i.focus(window, cx));
|
|
||||||
}
|
}
|
||||||
ModalFocus::TagsInput => {
|
ModalFocus::TagsInput => {
|
||||||
self.entities
|
self.entities
|
||||||
@@ -2126,7 +2149,7 @@ impl gpui::Render for TaskDetailModal {
|
|||||||
&self.entities.annotation_input,
|
&self.entities.annotation_input,
|
||||||
&self.entities.description_input,
|
&self.entities.description_input,
|
||||||
&self.entities.project_input,
|
&self.entities.project_input,
|
||||||
&self.entities.due_input,
|
&self.entities.due_picker,
|
||||||
&self.entities.tags_input,
|
&self.entities.tags_input,
|
||||||
&self.entities.status_dropdown,
|
&self.entities.status_dropdown,
|
||||||
&self.entities.priority_dropdown,
|
&self.entities.priority_dropdown,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
|
|
||||||
use crate::components::button::Dropdown;
|
use crate::components::button::Dropdown;
|
||||||
|
use crate::components::date_picker::DatePicker;
|
||||||
use crate::components::input::Input;
|
use crate::components::input::Input;
|
||||||
use crate::components::modal::ModalFrame;
|
use crate::components::modal::ModalFrame;
|
||||||
use crate::theme::ActiveTheme;
|
use crate::theme::ActiveTheme;
|
||||||
@@ -15,7 +16,7 @@ pub(super) fn render_task_detail_modal(
|
|||||||
annotation_input: &gpui::Entity<Input>,
|
annotation_input: &gpui::Entity<Input>,
|
||||||
description_input: &gpui::Entity<Input>,
|
description_input: &gpui::Entity<Input>,
|
||||||
project_input: &gpui::Entity<Input>,
|
project_input: &gpui::Entity<Input>,
|
||||||
due_input: &gpui::Entity<Input>,
|
due_picker: &gpui::Entity<DatePicker>,
|
||||||
tags_input: &gpui::Entity<Input>,
|
tags_input: &gpui::Entity<Input>,
|
||||||
status_dropdown: &gpui::Entity<Dropdown>,
|
status_dropdown: &gpui::Entity<Dropdown>,
|
||||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||||
@@ -57,7 +58,7 @@ pub(super) fn render_task_detail_modal(
|
|||||||
annotation_input,
|
annotation_input,
|
||||||
description_input,
|
description_input,
|
||||||
project_input,
|
project_input,
|
||||||
due_input,
|
due_picker,
|
||||||
tags_input,
|
tags_input,
|
||||||
status_dropdown,
|
status_dropdown,
|
||||||
priority_dropdown,
|
priority_dropdown,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::components::action_button::ActionButton;
|
|||||||
use crate::components::button::Dropdown;
|
use crate::components::button::Dropdown;
|
||||||
use crate::components::chip::{Chip, ChipVariant};
|
use crate::components::chip::{Chip, ChipVariant};
|
||||||
use crate::components::confirm_dialog::ConfirmDialog;
|
use crate::components::confirm_dialog::ConfirmDialog;
|
||||||
|
use crate::components::date_picker::DatePicker;
|
||||||
use crate::components::field_row::{FieldRow, KvRow};
|
use crate::components::field_row::{FieldRow, KvRow};
|
||||||
use crate::components::input::Input;
|
use crate::components::input::Input;
|
||||||
use crate::components::label::Label;
|
use crate::components::label::Label;
|
||||||
@@ -116,7 +117,7 @@ pub(super) fn render_task_detail_panel<OnCloseClick>(
|
|||||||
annotation_input: &gpui::Entity<Input>,
|
annotation_input: &gpui::Entity<Input>,
|
||||||
description_input: &gpui::Entity<Input>,
|
description_input: &gpui::Entity<Input>,
|
||||||
project_input: &gpui::Entity<Input>,
|
project_input: &gpui::Entity<Input>,
|
||||||
due_input: &gpui::Entity<Input>,
|
due_picker: &gpui::Entity<DatePicker>,
|
||||||
tags_input: &gpui::Entity<Input>,
|
tags_input: &gpui::Entity<Input>,
|
||||||
status_dropdown: &gpui::Entity<Dropdown>,
|
status_dropdown: &gpui::Entity<Dropdown>,
|
||||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||||
@@ -246,7 +247,7 @@ where
|
|||||||
&priority_label,
|
&priority_label,
|
||||||
description_input,
|
description_input,
|
||||||
project_input,
|
project_input,
|
||||||
due_input,
|
due_picker,
|
||||||
status_dropdown,
|
status_dropdown,
|
||||||
priority_dropdown,
|
priority_dropdown,
|
||||||
theme,
|
theme,
|
||||||
@@ -468,7 +469,7 @@ fn render_overview_section(
|
|||||||
priority_label: &str,
|
priority_label: &str,
|
||||||
description_input: &gpui::Entity<Input>,
|
description_input: &gpui::Entity<Input>,
|
||||||
project_input: &gpui::Entity<Input>,
|
project_input: &gpui::Entity<Input>,
|
||||||
due_input: &gpui::Entity<Input>,
|
due_picker: &gpui::Entity<DatePicker>,
|
||||||
status_dropdown: &gpui::Entity<Dropdown>,
|
status_dropdown: &gpui::Entity<Dropdown>,
|
||||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
@@ -588,14 +589,33 @@ fn render_overview_section(
|
|||||||
|
|
||||||
let due_value = if is_editing {
|
let due_value = if is_editing {
|
||||||
let focused = modal_focus == ModalFocus::Due;
|
let focused = modal_focus == ModalFocus::Due;
|
||||||
|
let theme_clone = theme.clone();
|
||||||
|
let picker_clone = due_picker.clone();
|
||||||
|
|
||||||
crate::ui::focus_wrap(due_input.clone(), focused, theme)
|
gpui::div()
|
||||||
|
.relative()
|
||||||
|
.when(focused, |d| {
|
||||||
|
d.border_2()
|
||||||
|
.border_color(theme_clone.focus_ring)
|
||||||
|
.rounded_md()
|
||||||
|
.p_px()
|
||||||
|
})
|
||||||
.on_mouse_down(
|
.on_mouse_down(
|
||||||
gpui::MouseButton::Left,
|
gpui::MouseButton::Left,
|
||||||
cx.listener(|modal, _, window, cx| {
|
cx.listener(move |modal, _, window, cx| {
|
||||||
modal.set_modal_focus_and_edit(ModalFocus::Due, Some(window), cx);
|
modal.set_modal_focus(ModalFocus::Due, Some(window), cx);
|
||||||
|
let picker = picker_clone.clone();
|
||||||
|
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
picker.update(cx, |d, cx| {
|
||||||
|
if !d.is_open() {
|
||||||
|
d.open(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(due_picker.clone())
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
value_label(due_text, theme.foreground)
|
value_label(due_text, theme.foreground)
|
||||||
|
|||||||
Reference in New Issue
Block a user