Skip to main content

things3_cloud/ui/views/
anytime.rs

1use crate::common::ICONS;
2use crate::ids::ThingsId;
3use crate::store::{Task, ThingsStore};
4use crate::ui::components::empty_text::EmptyText;
5use crate::ui::components::task_group::{TaskGroup, TaskGroupHeader};
6use crate::ui::components::tasks::TaskOptions;
7use iocraft::prelude::*;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11const MAX_GROUP_ITEMS: usize = 3;
12const LIST_INDENT: u32 = 2;
13
14#[derive(Default)]
15struct AreaTaskGroup {
16    tasks: Vec<usize>,
17    projects: Vec<(ThingsId, Vec<usize>)>,
18    project_pos: HashMap<ThingsId, usize>,
19}
20
21#[derive(Default)]
22struct Grouped {
23    unscoped: Vec<usize>,
24    project_only: Vec<(ThingsId, Vec<usize>)>,
25    by_area: Vec<(ThingsId, AreaTaskGroup)>,
26}
27
28fn ensure_area_group(
29    grouped: &mut Grouped,
30    area_pos: &mut HashMap<ThingsId, usize>,
31    area_uuid: &ThingsId,
32) -> usize {
33    if let Some(i) = area_pos.get(area_uuid).copied() {
34        return i;
35    }
36    let i = grouped.by_area.len();
37    grouped
38        .by_area
39        .push((area_uuid.clone(), AreaTaskGroup::default()));
40    area_pos.insert(area_uuid.clone(), i);
41    i
42}
43
44fn ensure_project_group(
45    grouped: &mut Vec<(ThingsId, Vec<usize>)>,
46    project_pos: &mut HashMap<ThingsId, usize>,
47    project_uuid: &ThingsId,
48) -> usize {
49    if let Some(i) = project_pos.get(project_uuid).copied() {
50        return i;
51    }
52    let i = grouped.len();
53    grouped.push((project_uuid.clone(), Vec::new()));
54    project_pos.insert(project_uuid.clone(), i);
55    i
56}
57
58fn group_items(items: &[Task], store: &ThingsStore) -> Grouped {
59    let mut grouped = Grouped::default();
60    let mut project_only_pos: HashMap<ThingsId, usize> = HashMap::new();
61    let mut by_area_pos: HashMap<ThingsId, usize> = HashMap::new();
62
63    for (idx, task) in items.iter().enumerate() {
64        let project_uuid = store.effective_project_uuid(task);
65        let area_uuid = store.effective_area_uuid(task);
66
67        match (project_uuid, area_uuid) {
68            (Some(project_uuid), Some(area_uuid)) => {
69                let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
70                let area_group = &mut grouped.by_area[area_idx].1;
71                let project_idx = ensure_project_group(
72                    &mut area_group.projects,
73                    &mut area_group.project_pos,
74                    &project_uuid,
75                );
76                area_group.projects[project_idx].1.push(idx);
77            }
78            (Some(project_uuid), None) => {
79                let project_idx = ensure_project_group(
80                    &mut grouped.project_only,
81                    &mut project_only_pos,
82                    &project_uuid,
83                );
84                grouped.project_only[project_idx].1.push(idx);
85            }
86            (None, Some(area_uuid)) => {
87                let area_idx = ensure_area_group(&mut grouped, &mut by_area_pos, &area_uuid);
88                grouped.by_area[area_idx].1.tasks.push(idx);
89            }
90            (None, None) => grouped.unscoped.push(idx),
91        }
92    }
93
94    grouped
95}
96
97fn id_prefix_len(store: &ThingsStore, items: &[Task], grouped: &Grouped) -> usize {
98    let mut ids: Vec<ThingsId> = items.iter().map(|t| t.uuid.clone()).collect();
99    for (project_uuid, _) in &grouped.project_only {
100        ids.push(project_uuid.clone());
101    }
102    for (area_uuid, area_group) in &grouped.by_area {
103        ids.push(area_uuid.clone());
104        for (project_uuid, _) in &area_group.projects {
105            ids.push(project_uuid.clone());
106        }
107    }
108    store.unique_prefix_length(&ids)
109}
110
111fn limited(tasks: &[usize]) -> (&[usize], usize) {
112    let shown = tasks.len().min(MAX_GROUP_ITEMS);
113    (&tasks[..shown], tasks.len().saturating_sub(shown))
114}
115
116#[derive(Default, Props)]
117pub struct AnytimeViewProps<'a> {
118    pub items: Option<&'a Vec<Task>>,
119    pub detailed: bool,
120}
121
122#[component]
123pub fn AnytimeView<'a>(hooks: Hooks, props: &AnytimeViewProps<'a>) -> impl Into<AnyElement<'a>> {
124    let store = hooks.use_context::<Arc<ThingsStore>>().clone();
125    let Some(items) = props.items else {
126        return element! { Text(content: "") }.into_any();
127    };
128
129    let content: AnyElement<'a> = {
130        if items.is_empty() {
131            element! { EmptyText(content: "Anytime is empty.") }.into_any()
132        } else {
133            let grouped = group_items(items, store.as_ref());
134            let prefix_len = id_prefix_len(store.as_ref(), items, &grouped);
135            let options = TaskOptions {
136                detailed: props.detailed,
137                show_project: false,
138                show_area: false,
139                show_today_markers: true,
140                show_staged_today_marker: false,
141            };
142
143            struct BlockSpec<'b> {
144                header: Option<TaskGroupHeader>,
145                items: Vec<&'b Task>,
146                indent_under_header: u16,
147                hidden_count: usize,
148                extra_left: u32,
149            }
150
151            enum Entry<'b> {
152                Sep,
153                Block(BlockSpec<'b>),
154            }
155
156            let mut blocks: Vec<AnyElement<'a>> = Vec::new();
157            let mut entries: Vec<Entry<'a>> = Vec::new();
158            let mut first = true;
159
160            if !grouped.unscoped.is_empty() {
161                entries.push(Entry::Block(BlockSpec {
162                    header: None,
163                    items: grouped.unscoped.iter().map(|&i| &items[i]).collect(),
164                    indent_under_header: 0u16,
165                    hidden_count: 0usize,
166                    extra_left: 0,
167                }));
168                first = false;
169            }
170
171            for (project_uuid, tasks) in &grouped.project_only {
172                if !first {
173                    entries.push(Entry::Sep);
174                }
175                let (shown, hidden) = limited(tasks);
176                entries.push(Entry::Block(BlockSpec {
177                    header: Some(TaskGroupHeader::Project {
178                        project_uuid: project_uuid.clone(),
179                        title: store.resolve_project_title(project_uuid),
180                        id_prefix_len: prefix_len,
181                    }),
182                    items: shown.iter().map(|&i| &items[i]).collect(),
183                    indent_under_header: 2u16,
184                    hidden_count: hidden,
185                    extra_left: 0,
186                }));
187                first = false;
188            }
189
190            for (area_uuid, area_group) in &grouped.by_area {
191                if !first {
192                    entries.push(Entry::Sep);
193                }
194
195                let (shown_area_tasks, hidden_area_tasks) = limited(&area_group.tasks);
196
197                entries.push(Entry::Block(BlockSpec {
198                    header: Some(TaskGroupHeader::Area {
199                        area_uuid: area_uuid.clone(),
200                        title: store.resolve_area_title(area_uuid),
201                        id_prefix_len: prefix_len,
202                    }),
203                    items: shown_area_tasks.iter().map(|&i| &items[i]).collect(),
204                    indent_under_header: 2u16,
205                    hidden_count: hidden_area_tasks,
206                    extra_left: 0,
207                }));
208
209                for (project_uuid, project_tasks) in &area_group.projects {
210                    let (shown, hidden) = limited(project_tasks);
211                    entries.push(Entry::Block(BlockSpec {
212                        header: Some(TaskGroupHeader::Project {
213                            project_uuid: project_uuid.clone(),
214                            title: store.resolve_project_title(project_uuid),
215                            id_prefix_len: prefix_len,
216                        }),
217                        items: shown.iter().map(|&i| &items[i]).collect(),
218                        indent_under_header: 2u16,
219                        hidden_count: hidden,
220                        extra_left: 2,
221                    }));
222                }
223
224                first = false;
225            }
226
227            for entry in entries {
228                match entry {
229                    Entry::Sep => {
230                        blocks.push(
231                            element! { Text(content: "", wrap: TextWrap::NoWrap) }.into_any(),
232                        );
233                    }
234                    Entry::Block(spec) => {
235                        let block = element! {
236                            TaskGroup(
237                                header: spec.header,
238                                items: spec.items,
239                                id_prefix_len: prefix_len,
240                                options: options,
241                                indent_under_header: spec.indent_under_header,
242                                hidden_count: spec.hidden_count,
243                            )
244                        }
245                        .into_any();
246
247                        if spec.extra_left > 0 {
248                            blocks.push(
249                                element! {
250                                    View(padding_left: spec.extra_left) { #(Some(block)) }
251                                }
252                                .into_any(),
253                            );
254                        } else {
255                            blocks.push(block);
256                        }
257                    }
258                }
259            }
260
261            element! {
262                View(flex_direction: FlexDirection::Column, gap: 1) {
263                    Text(
264                        content: format!("{} Anytime  ({} tasks)", ICONS.anytime, items.len()),
265                        wrap: TextWrap::NoWrap,
266                        color: Color::Cyan,
267                        weight: Weight::Bold,
268                    )
269                    View(flex_direction: FlexDirection::Column, padding_left: LIST_INDENT) {
270                        #(blocks)
271                    }
272                }
273            }
274            .into_any()
275        }
276    };
277
278    content
279}