1use crate::{
2 document::TextDocument,
3 editor::EditorError,
4 layout::TextEngine,
5 outline::{OutlineLayout, TITLE_WIDTH, outline_layout},
6};
7use onestore::ExGuid;
8use onestore::page::text::EditError;
9use onestore::page::{Definition, Outline, PageParagraph};
10use std::collections::BTreeMap;
11
12pub use onestore::page::DateField;
13
14#[derive(Clone)]
15pub struct PageDate {
16 timestamp: u64,
17 source: Outline,
18 layout: OutlineLayout,
19}
20
21impl PageDate {
22 /// FILETIME ticks, in 100 ns units since 1601-01-01 UTC.
23 pub fn timestamp(&self) -> u64 {
24 self.timestamp
25 }
26
27 pub fn source(&self) -> &Outline {
28 &self.source
29 }
30
31 pub fn layout(&self) -> &OutlineLayout {
32 &self.layout
33 }
34
35 pub(crate) fn fields(&self) -> impl Iterator<Item = (DateField, &PageParagraph)> {
36 let swapped = self
37 .source
38 .paragraphs
39 .iter()
40 .enumerate()
41 .any(|(index, paragraph)| {
42 matches!(
43 (index, paragraph.text().unwrap().date_field),
44 (0, Some(DateField::Time)) | (1, Some(DateField::Date))
45 )
46 });
47 self.source
48 .paragraphs
49 .iter()
50 .enumerate()
51 .map(move |(index, paragraph)| {
52 let field =
53 paragraph
54 .text()
55 .unwrap()
56 .date_field
57 .unwrap_or(if (index == 0) != swapped {
58 DateField::Date
59 } else {
60 DateField::Time
61 });
62 (field, paragraph)
63 })
64 }
65
66 pub fn new(
67 timestamp: u64,
68 source: Outline,
69 engine: &mut TextEngine,
70 definitions: &BTreeMap<ExGuid, Definition>,
71 ) -> Result<Self, EditorError> {
72 if source.title || source.paragraphs.is_empty() || source.paragraphs.len() > 2 {
73 return Err(EditError::UnsupportedContent.into());
74 }
75 TextDocument::from_nodes(source.paragraphs.clone())?;
76 // A date field is one plain run in a plain paragraph.
77 if source.paragraphs.iter().any(|paragraph| {
78 !paragraph.lists.is_empty()
79 || !paragraph.tags.is_empty()
80 || paragraph.collapsed
81 || paragraph.parent.is_some()
82 || paragraph
83 .text()
84 .is_none_or(|text| !text.tags.is_empty() || text.text.spans().len() != 1)
85 }) {
86 return Err(EditError::UnsupportedContent.into());
87 }
88 let layout = outline_layout(&source, engine, definitions, TITLE_WIDTH)?;
89 let date = Self {
90 timestamp,
91 source,
92 layout,
93 };
94 let mut seen = [false; 2];
95 for (field, _) in date.fields() {
96 if std::mem::replace(&mut seen[field as usize], true) {
97 return Err(EditError::UnsupportedContent.into());
98 }
99 }
100 Ok(date)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::editor::{CanvasEditor, TextOutline};
108 use onestore::page::text::Paragraph;
109
110 #[test]
111 fn date_and_text_edits_share_history_without_changing_identities_or_selection() {
112 let mut engine = TextEngine::default();
113 let definitions = BTreeMap::new();
114 let body = TextOutline::new(
115 &mut engine,
116 TextDocument::new(vec![Paragraph::new("Body".into(), Default::default())]).unwrap(),
117 240.0,
118 [36.0, 90.0],
119 )
120 .unwrap();
121 let duplicate = PageDate::new(1, body.snapshot(), &mut engine, &definitions).unwrap();
122 assert!(
123 CanvasEditor::from_text_outlines(vec![body.clone()], BTreeMap::new(), Some(duplicate))
124 .is_err()
125 );
126 let source = TextOutline::new(
127 &mut engine,
128 TextDocument::new(
129 ["Monday, September 07, 2026", "6:14 AM"]
130 .map(|text| Paragraph::new(text.into(), Default::default()))
131 .into(),
132 )
133 .unwrap(),
134 468.0,
135 [0.0; 2],
136 )
137 .unwrap()
138 .snapshot();
139 let original = source.paragraphs.clone();
140 let date = PageDate::new(100_123_456, source, &mut engine, &definitions).unwrap();
141 let original_layout = date.layout.paragraphs[0].text.id();
142 let mut editor =
143 CanvasEditor::from_text_outlines(vec![body], definitions, Some(date)).unwrap();
144 let body = editor.active_outline().document().clone();
145 editor.compose(&mut engine, "new ".into(), 4..4).unwrap();
146 let selection = editor.selection();
147 let text = ["Wednesday, September 09, 2026".into(), "6:14 AM".into()];
148 assert!(
149 editor
150 .change_date(&mut engine, 200_123_456, text.clone())
151 .unwrap()
152 );
153 assert!(editor.marked_range().is_none());
154 assert_eq!(editor.selection(), selection);
155 let changed = editor.date().unwrap().source.paragraphs.clone();
156 for (old, new) in original.iter().zip(&changed) {
157 assert_eq!(old.id, new.id);
158 assert_eq!(old.text().unwrap().id, new.text().unwrap().id);
159 let mut restored = new.clone();
160 restored.text_mut().unwrap().text = old.text().unwrap().text.clone();
161 assert_eq!(&restored, old);
162 }
163 assert!(editor.undo(&mut engine).unwrap());
164 assert_eq!(editor.date().unwrap().timestamp, 100_123_456);
165 assert_eq!(editor.date().unwrap().source.paragraphs, original);
166 assert_eq!(
167 editor.date().unwrap().layout.paragraphs[0].text.id(),
168 original_layout
169 );
170 assert_eq!(editor.selection(), selection);
171 assert!(editor.undo(&mut engine).unwrap());
172 assert_eq!(editor.active_outline().document(), &body);
173 assert!(editor.redo(&mut engine).unwrap());
174 assert!(editor.redo(&mut engine).unwrap());
175 assert_eq!(editor.date().unwrap().source.paragraphs, changed);
176 assert!(!editor.change_date(&mut engine, 200_123_456, text).unwrap());
177 editor.undo(&mut engine).unwrap();
178 editor
179 .change_date(
180 &mut engine,
181 300_123_456,
182 ["Friday".into(), "7:00 AM".into()],
183 )
184 .unwrap();
185 assert!(!editor.redo(&mut engine).unwrap());
186 }
187
188 #[test]
189 fn explicit_roles_control_reordered_and_time_only_fields() {
190 let mut engine = TextEngine::default();
191 let source = TextOutline::new(
192 &mut engine,
193 TextDocument::new(
194 ["6:14 AM", "Monday"]
195 .map(|text| Paragraph::new(text.into(), Default::default()))
196 .into(),
197 )
198 .unwrap(),
199 468.0,
200 [0.0; 2],
201 )
202 .unwrap()
203 .snapshot();
204 for count in [1, 2] {
205 let mut source = source.clone();
206 source.paragraphs.truncate(count);
207 source.paragraphs[0].text_mut().unwrap().date_field = Some(DateField::Time);
208 let date = PageDate::new(1, source, &mut engine, &BTreeMap::new()).unwrap();
209 assert_eq!(date.fields().next().unwrap().0, DateField::Time);
210 let mut editor = CanvasEditor::new(
211 &mut engine,
212 TextDocument::new(vec![Paragraph::new("Body".into(), Default::default())]).unwrap(),
213 200.0,
214 )
215 .unwrap();
216 let outlines = editor.outlines().to_vec();
217 editor =
218 CanvasEditor::from_text_outlines(outlines, BTreeMap::new(), Some(date)).unwrap();
219 editor
220 .change_date(&mut engine, 2, ["New date".into(), "New time".into()])
221 .unwrap();
222 assert_eq!(
223 editor.date().unwrap().source.paragraphs[0]
224 .text()
225 .unwrap()
226 .text
227 .text(),
228 "New time"
229 );
230 if count == 2 {
231 assert_eq!(
232 editor.date().unwrap().source.paragraphs[1]
233 .text()
234 .unwrap()
235 .text
236 .text(),
237 "New date"
238 );
239 let mut duplicate = editor.date().unwrap().source.clone();
240 duplicate.paragraphs[1].text_mut().unwrap().date_field = Some(DateField::Time);
241 assert!(PageDate::new(3, duplicate, &mut engine, &BTreeMap::new()).is_err());
242 }
243 }
244 }
245}