Add date picker and task improvements
This commit is contained in:
Generated
+1
-1
@@ -6497,7 +6497,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "task-warrior-gpui"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"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>) {
|
||||
if let Some(suggest) = &self.suggest {
|
||||
self.suggestions = suggest(&self.value);
|
||||
self.active_suggestion = 0;
|
||||
self.suggestions_open = !self.suggestions.is_empty() && !self.value.is_empty();
|
||||
cx.notify();
|
||||
}
|
||||
self.open_suggestions(cx);
|
||||
}
|
||||
|
||||
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 {
|
||||
self.suggestions = suggest(&self.value);
|
||||
self.active_suggestion = 0;
|
||||
self.suggestions_open = !self.suggestions.is_empty() && !self.value.is_empty();
|
||||
self.suggestions_open = !self.suggestions.is_empty();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod action_button;
|
||||
pub mod button;
|
||||
pub mod chip;
|
||||
pub mod confirm_dialog;
|
||||
pub mod date_picker;
|
||||
pub mod dialog;
|
||||
pub mod divider;
|
||||
pub mod field_row;
|
||||
|
||||
@@ -21,7 +21,6 @@ impl FocusMap {
|
||||
focus,
|
||||
ModalFocus::Description
|
||||
| ModalFocus::Project
|
||||
| ModalFocus::Due
|
||||
| ModalFocus::TagsInput
|
||||
| ModalFocus::AnnotationsInput
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::components::button::{Dropdown, DropdownItem};
|
||||
use crate::components::date_picker::DatePicker;
|
||||
use crate::components::input::Input;
|
||||
use crate::components::toast::{ToastGlobal, ToastKind};
|
||||
use crate::keymap::{Command, CommandDispatcher, ContextId};
|
||||
@@ -45,7 +46,7 @@ struct ModalEntities {
|
||||
annotation_input: gpui::Entity<Input>,
|
||||
description_input: gpui::Entity<Input>,
|
||||
project_input: gpui::Entity<Input>,
|
||||
due_input: gpui::Entity<Input>,
|
||||
due_picker: gpui::Entity<DatePicker>,
|
||||
tags_input: gpui::Entity<Input>,
|
||||
status_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(
|
||||
move |query| {
|
||||
let query = query.trim();
|
||||
if query.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Ok(list) = suggestions.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let needle = query.to_lowercase();
|
||||
let mut candidates = Vec::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 prefix_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 on_change_entity = modal_entity.clone();
|
||||
@@ -313,7 +337,7 @@ impl TaskDetailModal {
|
||||
annotation_input,
|
||||
description_input,
|
||||
project_input,
|
||||
due_input,
|
||||
due_picker,
|
||||
tags_input,
|
||||
status_dropdown,
|
||||
priority_dropdown,
|
||||
@@ -638,8 +662,9 @@ impl TaskDetailModal {
|
||||
input.set_value_silent(form.project, cx);
|
||||
});
|
||||
|
||||
self.entities.due_input.update(cx, |input, cx| {
|
||||
input.set_value_silent(form.due, cx);
|
||||
let due_date = NaiveDate::parse_from_str(form.due.trim(), DATE_FORMAT).ok();
|
||||
self.entities.due_picker.update(cx, |picker, cx| {
|
||||
picker.set_value(due_date, cx);
|
||||
});
|
||||
|
||||
self.entities.tags_input.update(cx, |input, cx| {
|
||||
@@ -685,12 +710,6 @@ impl TaskDetailModal {
|
||||
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>) {
|
||||
self.state.form.tag_draft = value.to_string();
|
||||
cx.notify();
|
||||
@@ -721,7 +740,6 @@ impl TaskDetailModal {
|
||||
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.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.annotations.draft = self
|
||||
.entities
|
||||
@@ -903,8 +921,8 @@ impl TaskDetailModal {
|
||||
.project_input
|
||||
.update(cx, |input, cx| input.blur(window, cx));
|
||||
self.entities
|
||||
.due_input
|
||||
.update(cx, |input, cx| input.blur(window, cx));
|
||||
.due_picker
|
||||
.update(cx, |picker, cx| picker.close(cx));
|
||||
self.entities
|
||||
.tags_input
|
||||
.update(cx, |input, cx| input.blur(window, cx));
|
||||
@@ -989,11 +1007,6 @@ impl TaskDetailModal {
|
||||
.project_input
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
}
|
||||
ModalFocus::Due => {
|
||||
self.entities
|
||||
.due_input
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
}
|
||||
ModalFocus::TagsInput => {
|
||||
self.entities
|
||||
.tags_input
|
||||
@@ -1004,13 +1017,16 @@ impl TaskDetailModal {
|
||||
.annotation_input
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
}
|
||||
ModalFocus::None | ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {}
|
||||
ModalFocus::None
|
||||
| ModalFocus::StatusDropdown
|
||||
| ModalFocus::PriorityDropdown
|
||||
| ModalFocus::Due => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match focus {
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due => {
|
||||
window.focus(&self.form_focus_handle);
|
||||
}
|
||||
ModalFocus::None => {
|
||||
@@ -1018,7 +1034,6 @@ impl TaskDetailModal {
|
||||
}
|
||||
ModalFocus::Description
|
||||
| ModalFocus::Project
|
||||
| ModalFocus::Due
|
||||
| ModalFocus::TagsInput
|
||||
| ModalFocus::AnnotationsInput => {}
|
||||
}
|
||||
@@ -1040,12 +1055,14 @@ impl TaskDetailModal {
|
||||
self.entities
|
||||
.priority_dropdown
|
||||
.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 {
|
||||
let status_open = self.entities.status_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>) {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// 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>) {
|
||||
let select_next = |d: &gpui::Entity<Dropdown>, cx: &mut gpui::Context<Self>| {
|
||||
d.update(cx, |d, cx| {
|
||||
@@ -1802,7 +1830,7 @@ impl TaskDetailModal {
|
||||
if self.state.mode == ModalMode::Edit
|
||||
&& matches!(
|
||||
self.state.modal_focus,
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due
|
||||
)
|
||||
{
|
||||
if self.state.edit_state == EditState::DropdownOpen {
|
||||
@@ -1911,13 +1939,12 @@ impl TaskDetailModal {
|
||||
}
|
||||
|
||||
match self.state.modal_focus {
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown => {
|
||||
ModalFocus::StatusDropdown | ModalFocus::PriorityDropdown | ModalFocus::Due => {
|
||||
self.state.edit_state = EditState::DropdownOpen;
|
||||
self.toggle_focused_dropdown(cx);
|
||||
}
|
||||
ModalFocus::Description
|
||||
| ModalFocus::Project
|
||||
| ModalFocus::Due
|
||||
| ModalFocus::TagsInput
|
||||
| ModalFocus::AnnotationsInput => {
|
||||
self.state.edit_state = EditState::Editing;
|
||||
@@ -1930,14 +1957,10 @@ impl TaskDetailModal {
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
}
|
||||
ModalFocus::Project => {
|
||||
self.entities
|
||||
.project_input
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
}
|
||||
ModalFocus::Due => {
|
||||
self.entities
|
||||
.due_input
|
||||
.update(cx, |i, cx| i.focus(window, cx));
|
||||
self.entities.project_input.update(cx, |i, cx| {
|
||||
i.focus(window, cx);
|
||||
i.open_suggestions(cx);
|
||||
});
|
||||
}
|
||||
ModalFocus::TagsInput => {
|
||||
self.entities
|
||||
@@ -2126,7 +2149,7 @@ impl gpui::Render for TaskDetailModal {
|
||||
&self.entities.annotation_input,
|
||||
&self.entities.description_input,
|
||||
&self.entities.project_input,
|
||||
&self.entities.due_input,
|
||||
&self.entities.due_picker,
|
||||
&self.entities.tags_input,
|
||||
&self.entities.status_dropdown,
|
||||
&self.entities.priority_dropdown,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use gpui::prelude::*;
|
||||
|
||||
use crate::components::button::Dropdown;
|
||||
use crate::components::date_picker::DatePicker;
|
||||
use crate::components::input::Input;
|
||||
use crate::components::modal::ModalFrame;
|
||||
use crate::theme::ActiveTheme;
|
||||
@@ -15,7 +16,7 @@ pub(super) fn render_task_detail_modal(
|
||||
annotation_input: &gpui::Entity<Input>,
|
||||
description_input: &gpui::Entity<Input>,
|
||||
project_input: &gpui::Entity<Input>,
|
||||
due_input: &gpui::Entity<Input>,
|
||||
due_picker: &gpui::Entity<DatePicker>,
|
||||
tags_input: &gpui::Entity<Input>,
|
||||
status_dropdown: &gpui::Entity<Dropdown>,
|
||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||
@@ -57,7 +58,7 @@ pub(super) fn render_task_detail_modal(
|
||||
annotation_input,
|
||||
description_input,
|
||||
project_input,
|
||||
due_input,
|
||||
due_picker,
|
||||
tags_input,
|
||||
status_dropdown,
|
||||
priority_dropdown,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::components::action_button::ActionButton;
|
||||
use crate::components::button::Dropdown;
|
||||
use crate::components::chip::{Chip, ChipVariant};
|
||||
use crate::components::confirm_dialog::ConfirmDialog;
|
||||
use crate::components::date_picker::DatePicker;
|
||||
use crate::components::field_row::{FieldRow, KvRow};
|
||||
use crate::components::input::Input;
|
||||
use crate::components::label::Label;
|
||||
@@ -116,7 +117,7 @@ pub(super) fn render_task_detail_panel<OnCloseClick>(
|
||||
annotation_input: &gpui::Entity<Input>,
|
||||
description_input: &gpui::Entity<Input>,
|
||||
project_input: &gpui::Entity<Input>,
|
||||
due_input: &gpui::Entity<Input>,
|
||||
due_picker: &gpui::Entity<DatePicker>,
|
||||
tags_input: &gpui::Entity<Input>,
|
||||
status_dropdown: &gpui::Entity<Dropdown>,
|
||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||
@@ -246,7 +247,7 @@ where
|
||||
&priority_label,
|
||||
description_input,
|
||||
project_input,
|
||||
due_input,
|
||||
due_picker,
|
||||
status_dropdown,
|
||||
priority_dropdown,
|
||||
theme,
|
||||
@@ -468,7 +469,7 @@ fn render_overview_section(
|
||||
priority_label: &str,
|
||||
description_input: &gpui::Entity<Input>,
|
||||
project_input: &gpui::Entity<Input>,
|
||||
due_input: &gpui::Entity<Input>,
|
||||
due_picker: &gpui::Entity<DatePicker>,
|
||||
status_dropdown: &gpui::Entity<Dropdown>,
|
||||
priority_dropdown: &gpui::Entity<Dropdown>,
|
||||
theme: &Theme,
|
||||
@@ -588,14 +589,33 @@ fn render_overview_section(
|
||||
|
||||
let due_value = if is_editing {
|
||||
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(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|modal, _, window, cx| {
|
||||
modal.set_modal_focus_and_edit(ModalFocus::Due, Some(window), cx);
|
||||
cx.listener(move |modal, _, 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()
|
||||
} else {
|
||||
value_label(due_text, theme.foreground)
|
||||
|
||||
Reference in New Issue
Block a user