| ... | @@ -0,0 +1,1336 @@ |
| 1 | //! OneNote's Home-ribbon commands: character formatting, lists, alignment and note tags. |
| 2 | |
| 3 | use super::*; |
| 4 | use onestore::document::{Format, Kind, Tag}; |
| 5 | use onestore::page::ParagraphContent; |
| 6 | use onestore::page::text::new_id; |
| 7 | |
| 8 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. |
| 9 | const AUTOMATIC: u32 = 0xff00_0000; |
| 10 | /// The one checkable note tag shape the page draws. |
| 11 | const CHECKBOX: u16 = 3; |
| 12 | |
| 13 | /// A toolbar command on the selection, or with only a caret, on the text typed there next. |
| 14 | #[derive(Clone, Debug, PartialEq)] |
| 15 | pub enum Formatting { |
| 16 | /// Removed where every selected character has it, applied to all of them otherwise. |
| 17 | Toggle(Toggle), |
| 18 | Font(String), |
| 19 | FontSize(f32), |
| 20 | /// A COLORREF, or `None` for automatic. |
| 21 | Color(Option<u32>), |
| 22 | /// A COLORREF, or `None` for no highlight. |
| 23 | Highlight(Option<u32>), |
| 24 | /// Returns text to its paragraph style. |
| 25 | Clear, |
| 26 | Bullets, |
| 27 | Numbering, |
| 28 | Indent, |
| 29 | Outdent, |
| 30 | Align(Alignment), |
| 31 | Tag(NoteTag), |
| 32 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. |
| 33 | Check, |
| 34 | } |
| 35 | |
| 36 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 37 | pub enum Toggle { |
| 38 | Bold, |
| 39 | Italic, |
| 40 | Underline, |
| 41 | Strikethrough, |
| 42 | /// Excludes superscript. |
| 43 | Subscript, |
| 44 | /// Excludes subscript. |
| 45 | Superscript, |
| 46 | } |
| 47 | |
| 48 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 49 | pub enum Alignment { |
| 50 | Left, |
| 51 | Center, |
| 52 | Right, |
| 53 | } |
| 54 | |
| 55 | /// OneNote 2010's default tags whose stored definition local evidence shows: To Do from a |
| 56 | /// native Ctrl+1 (`corpus/paragraph-edit/reconciliation/keyboard`), Question from notebooks |
| 57 | /// OneNote tagged (`corpus/private/exact-native`). |
| 58 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 59 | pub enum NoteTag { |
| 60 | ToDo, |
| 61 | Question, |
| 62 | } |
| 63 | |
| 64 | /// What the selection shows on the toolbar. |
| 65 | #[derive(Clone, Debug, PartialEq)] |
| 66 | pub struct FormatState { |
| 67 | /// Attributes every selected character has. |
| 68 | pub toggles: Vec<Toggle>, |
| 69 | /// The font and size every selected character shares. |
| 70 | pub font: Option<String>, |
| 71 | pub font_size: Option<f32>, |
| 72 | /// The alignment every selected paragraph shares. |
| 73 | pub alignment: Option<Alignment>, |
| 74 | /// Whether every selected paragraph is bulleted or numbered. |
| 75 | pub bullets: bool, |
| 76 | pub numbering: bool, |
| 77 | /// Tags every selected paragraph has. |
| 78 | pub tags: Vec<NoteTag>, |
| 79 | } |
| 80 | |
| 81 | impl Toggle { |
| 82 | const ALL: [Self; 6] = [ |
| 83 | Self::Bold, |
| 84 | Self::Italic, |
| 85 | Self::Underline, |
| 86 | Self::Strikethrough, |
| 87 | Self::Subscript, |
| 88 | Self::Superscript, |
| 89 | ]; |
| 90 | |
| 91 | fn get(self, format: &Format) -> Option<bool> { |
| 92 | match self { |
| 93 | Self::Bold => format.bold, |
| 94 | Self::Italic => format.italic, |
| 95 | Self::Underline => format.underline, |
| 96 | Self::Strikethrough => format.strike, |
| 97 | Self::Subscript => format.subscript, |
| 98 | Self::Superscript => format.superscript, |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | fn slot(self, format: &mut Format) -> &mut Option<bool> { |
| 103 | match self { |
| 104 | Self::Bold => &mut format.bold, |
| 105 | Self::Italic => &mut format.italic, |
| 106 | Self::Underline => &mut format.underline, |
| 107 | Self::Strikethrough => &mut format.strike, |
| 108 | Self::Subscript => &mut format.subscript, |
| 109 | Self::Superscript => &mut format.superscript, |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | impl NoteTag { |
| 115 | const ALL: [Self; 2] = [Self::ToDo, Self::Question]; |
| 116 | |
| 117 | fn definition(self) -> Definition { |
| 118 | let (label, action_type, shape) = match self { |
| 119 | Self::ToDo => ("To Do", 0, CHECKBOX), |
| 120 | Self::Question => ("Question", 2, 15), |
| 121 | }; |
| 122 | Definition { |
| 123 | kind: Kind::TagDefinition { |
| 124 | label: Some(label.into()), |
| 125 | action_type: Some(action_type), |
| 126 | shape: Some(shape), |
| 127 | color: None, |
| 128 | highlight: None, |
| 129 | }, |
| 130 | format: Format::default(), |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | /// Where a selection starts and ends, as text leaf identity and byte offset. |
| 136 | type Ends = [(ExGuid, usize); 2]; |
| 137 | |
| 138 | /// The selection's ends and the container range holding both; a selection ending at a |
| 139 | /// paragraph's start ends with the paragraph before. |
| 140 | fn selected( |
| 141 | document: &TextDocument, |
| 142 | selection: Selection, |
| 143 | ) -> Result<(Option<ExGuid>, Range<usize>, Ends), EditError> { |
| 144 | let [anchor, focus] = selection.positions; |
| 145 | let (start, mut end) = (anchor.min(focus), anchor.max(focus)); |
| 146 | if end.offset == 0 && end.paragraph > start.paragraph { |
| 147 | end.paragraph -= 1; |
| 148 | let text = document |
| 149 | .paragraph(end.paragraph) |
| 150 | .ok_or(EditError::InvalidRange)?; |
| 151 | end.offset = text.utf16_offset(text.text().len())?; |
| 152 | } |
| 153 | let (container, first, head) = document |
| 154 | .leaf(start.paragraph) |
| 155 | .ok_or(EditError::InvalidRange)?; |
| 156 | let (end_container, last, tail) = document |
| 157 | .leaf(end.paragraph) |
| 158 | .ok_or(EditError::InvalidRange)?; |
| 159 | let range = if container == end_container { |
| 160 | first..last + 1 |
| 161 | } else { |
| 162 | let root = |container: Option<ExGuid>, index| { |
| 163 | container.map_or(Ok(index), |cell| document.root(cell)) |
| 164 | }; |
| 165 | root(container, first)?..root(end_container, last)? + 1 |
| 166 | }; |
| 167 | let byte = |node: &PageParagraph, offset| node.text().unwrap().text.byte_offset(offset); |
| 168 | Ok(( |
| 169 | if container == end_container { |
| 170 | container |
| 171 | } else { |
| 172 | None |
| 173 | }, |
| 174 | range, |
| 175 | [ |
| 176 | (head.id, byte(head, start.offset)?), |
| 177 | (tail.id, byte(tail, end.offset)?), |
| 178 | ], |
| 179 | )) |
| 180 | } |
| 181 | |
| 182 | /// The text leaves of `nodes` from `start` through `end`, each with the byte range selected. |
| 183 | fn covered( |
| 184 | nodes: &[PageParagraph], |
| 185 | [start, end]: Ends, |
| 186 | ) -> impl Iterator<Item = (&PageParagraph, Range<usize>)> { |
| 187 | let mut done = false; |
| 188 | leaves(nodes, None) |
| 189 | .map(|(_, _, node)| node) |
| 190 | .skip_while(move |node| node.id != start.0) |
| 191 | .take_while(move |node| !std::mem::replace(&mut done, node.id == end.0)) |
| 192 | .map(move |node| { |
| 193 | let from = if node.id == start.0 { start.1 } else { 0 }; |
| 194 | let to = if node.id == end.0 { |
| 195 | end.1 |
| 196 | } else { |
| 197 | node.text().unwrap().text.text().len() |
| 198 | }; |
| 199 | (node, from..to) |
| 200 | }) |
| 201 | } |
| 202 | |
| 203 | fn leaves_mut(nodes: &mut [PageParagraph], change: &mut impl FnMut(&mut PageParagraph)) { |
| 204 | for node in nodes { |
| 205 | match &mut node.content { |
| 206 | ParagraphContent::Text(_) => change(node), |
| 207 | ParagraphContent::Table(table) => { |
| 208 | for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) { |
| 209 | leaves_mut(&mut cell.paragraphs, change); |
| 210 | } |
| 211 | } |
| 212 | _ => {} |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /// `text` with `change` applied to the formats of bytes `range`, or to its only format when empty. |
| 218 | fn restyle(text: &Paragraph, range: Range<usize>, change: impl Fn(&mut Format)) -> Paragraph { |
| 219 | if text.text().is_empty() { |
| 220 | let mut format = text.spans()[0].format.clone(); |
| 221 | change(&mut format); |
| 222 | return Paragraph::new(String::new(), format); |
| 223 | } |
| 224 | let mut runs = Vec::new(); |
| 225 | let mut start = 0; |
| 226 | for span in text.spans() { |
| 227 | let [from, to] = [range.start, range.end].map(|at| at.clamp(start, span.end)); |
| 228 | for (part, inside) in [ |
| 229 | (start..from, false), |
| 230 | (from..to, true), |
| 231 | (to..span.end, false), |
| 232 | ] { |
| 233 | if !part.is_empty() { |
| 234 | let mut format = span.format.clone(); |
| 235 | if inside { |
| 236 | change(&mut format); |
| 237 | } |
| 238 | runs.push((text.text()[part].to_owned(), format)); |
| 239 | } |
| 240 | } |
| 241 | start = span.end; |
| 242 | } |
| 243 | Paragraph::from_runs(runs) |
| 244 | } |
| 245 | |
| 246 | /// The formats of the visible characters in `range`, or of the paragraphs without text when the |
| 247 | /// selection holds no characters. |
| 248 | fn character_formats<'a>( |
| 249 | covered: impl Iterator<Item = (&'a PageParagraph, Range<usize>)>, |
| 250 | ) -> Vec<&'a Format> { |
| 251 | let mut characters = Vec::new(); |
| 252 | let mut blank = Vec::new(); |
| 253 | for (node, range) in covered { |
| 254 | let text = &node.text().unwrap().text; |
| 255 | if text.text().is_empty() { |
| 256 | blank.push(&text.spans()[0].format); |
| 257 | } |
| 258 | let mut start = 0; |
| 259 | for span in text.spans() { |
| 260 | if start < range.end && span.end > range.start && span.format.hidden != Some(true) { |
| 261 | characters.push(&span.format); |
| 262 | } |
| 263 | start = span.end; |
| 264 | } |
| 265 | } |
| 266 | if characters.is_empty() { |
| 267 | blank |
| 268 | } else { |
| 269 | characters |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | fn common<T: PartialEq>(mut values: impl Iterator<Item = Option<T>>) -> Option<T> { |
| 274 | let first = values.next()??; |
| 275 | values |
| 276 | .all(|value| value.as_ref() == Some(&first)) |
| 277 | .then_some(first) |
| 278 | } |
| 279 | |
| 280 | /// Seconds since 1980, as note tags date themselves. |
| 281 | fn time32() -> Option<u32> { |
| 282 | let now = std::time::SystemTime::now() |
| 283 | .duration_since(std::time::UNIX_EPOCH) |
| 284 | .ok()? |
| 285 | .as_secs(); |
| 286 | u32::try_from(now.checked_sub(315_532_800)?).ok() |
| 287 | } |
| 288 | |
| 289 | fn tags(node: &PageParagraph) -> impl Iterator<Item = &Tag> { |
| 290 | node.tags.iter().chain(&node.text().unwrap().tags) |
| 291 | } |
| 292 | |
| 293 | impl CanvasEditor { |
| 294 | /// The format text typed at `position` takes: the caret's pending format, else the text's |
| 295 | /// before it. |
| 296 | pub(super) fn typing_format(&self, position: TextPosition) -> Result<Format, EditError> { |
| 297 | if let Some((id, at, format)) = &self.pending |
| 298 | && *id == self.active_outline().id |
| 299 | && *at == position |
| 300 | { |
| 301 | return Ok(format.clone()); |
| 302 | } |
| 303 | Ok(self |
| 304 | .active_outline() |
| 305 | .document |
| 306 | .paragraph(position.paragraph) |
| 307 | .ok_or(EditError::InvalidRange)? |
| 308 | .format_at(position.offset)? |
| 309 | .clone()) |
| 310 | } |
| 311 | |
| 312 | fn list(&self, node: &PageParagraph) -> Option<Formatting> { |
| 313 | match &self.definitions.get(node.lists.last()?)?.kind { |
| 314 | Kind::List { format, .. } => Some( |
| 315 | if format |
| 316 | .as_ref() |
| 317 | .is_some_and(|format| format.contains('\u{fffd}')) |
| 318 | { |
| 319 | Formatting::Numbering |
| 320 | } else { |
| 321 | Formatting::Bullets |
| 322 | }, |
| 323 | ), |
| 324 | _ => None, |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | fn tag_kind(&self, tag: &Tag) -> Option<&Kind<'static>> { |
| 329 | Some(&self.definitions.get(tag.definition.as_ref()?)?.kind) |
| 330 | } |
| 331 | |
| 332 | pub fn format_state(&self) -> Result<FormatState, EditorError> { |
| 333 | let outline = self.active_outline(); |
| 334 | let (container, range, ends) = selected(&outline.document, outline.selection)?; |
| 335 | let nodes = &outline.document.container(container)?[range]; |
| 336 | let paragraphs = covered(nodes, ends) |
| 337 | .map(|(node, _)| node) |
| 338 | .collect::<Vec<_>>(); |
| 339 | let caret; |
| 340 | let formats = if ends[0] == ends[1] { |
| 341 | let [anchor, focus] = outline.selection.positions; |
| 342 | caret = self.typing_format(anchor.min(focus))?; |
| 343 | vec![&caret] |
| 344 | } else { |
| 345 | character_formats(covered(nodes, ends)) |
| 346 | }; |
| 347 | let list = |kind| { |
| 348 | paragraphs |
| 349 | .iter() |
| 350 | .all(|node| self.list(node).as_ref() == Some(&kind)) |
| 351 | }; |
| 352 | Ok(FormatState { |
| 353 | toggles: Toggle::ALL |
| 354 | .into_iter() |
| 355 | .filter(|toggle| { |
| 356 | formats |
| 357 | .iter() |
| 358 | .all(|format| toggle.get(format) == Some(true)) |
| 359 | }) |
| 360 | .collect(), |
| 361 | font: common(formats.iter().map(|format| format.font.clone())), |
| 362 | font_size: common(formats.iter().map(|format| format.font_size)), |
| 363 | alignment: common(paragraphs.iter().map(|node| { |
| 364 | Some( |
| 365 | match node.text().unwrap().text.spans()[0].format.alignment { |
| 366 | Some(1) => Alignment::Center, |
| 367 | Some(2) => Alignment::Right, |
| 368 | _ => Alignment::Left, |
| 369 | }, |
| 370 | ) |
| 371 | })), |
| 372 | bullets: list(Formatting::Bullets), |
| 373 | numbering: list(Formatting::Numbering), |
| 374 | tags: NoteTag::ALL |
| 375 | .into_iter() |
| 376 | .filter(|tag| { |
| 377 | let kind = tag.definition().kind; |
| 378 | paragraphs |
| 379 | .iter() |
| 380 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) |
| 381 | }) |
| 382 | .collect(), |
| 383 | }) |
| 384 | } |
| 385 | |
| 386 | /// Applies a toolbar command as one undo step that keeps the selection. |
| 387 | pub fn format( |
| 388 | &mut self, |
| 389 | engine: &mut TextEngine, |
| 390 | command: Formatting, |
| 391 | ) -> Result<(), EditorError> { |
| 392 | let outline = self.active_outline(); |
| 393 | let (id, title, selection) = (outline.id, outline.title, outline.selection); |
| 394 | let (container, range, ends) = selected(&outline.document, selection)?; |
| 395 | let mut replacement = outline.document.container(container)?[range.clone()].to_vec(); |
| 396 | let mut ranges = covered(&replacement, ends) |
| 397 | .map(|(node, range)| (node.id, range)) |
| 398 | .collect::<BTreeMap<_, _>>(); |
| 399 | let bases = |nodes: &[PageParagraph]| { |
| 400 | covered(nodes, ends) |
| 401 | .map(|(node, _)| Ok((node.id, self.style_format(node.style)?))) |
| 402 | .collect::<Result<BTreeMap<_, _>, EditError>>() |
| 403 | }; |
| 404 | match &command { |
| 405 | Formatting::Align(_) |
| 406 | | Formatting::Bullets |
| 407 | | Formatting::Numbering |
| 408 | | Formatting::Tag(_) |
| 409 | | Formatting::Check |
| 410 | if title => |
| 411 | { |
| 412 | return Ok(()); |
| 413 | } |
| 414 | Formatting::Indent | Formatting::Outdent => { |
| 415 | self.indent(engine, command == Formatting::Outdent)?; |
| 416 | return Ok(()); |
| 417 | } |
| 418 | Formatting::Align(alignment) => { |
| 419 | let bases = bases(&replacement)?; |
| 420 | leaves_mut(&mut replacement, &mut |node| { |
| 421 | if ranges.remove(&node.id).is_some() { |
| 422 | let value = match alignment { |
| 423 | Alignment::Left => bases[&node.id].alignment.map(|_| 0), |
| 424 | Alignment::Center => Some(1), |
| 425 | Alignment::Right => Some(2), |
| 426 | }; |
| 427 | let text = &mut node.text_mut().unwrap().text; |
| 428 | *text = restyle(text, 0..text.text().len(), |format| { |
| 429 | format.alignment = value; |
| 430 | }); |
| 431 | } |
| 432 | }); |
| 433 | } |
| 434 | Formatting::Bullets | Formatting::Numbering => { |
| 435 | let remove = covered(&replacement, ends) |
| 436 | .all(|(node, _)| self.list(node).as_ref() == Some(&command)); |
| 437 | let wanted = covered(&replacement, ends) |
| 438 | .map(|(node, _)| node) |
| 439 | .filter(|node| !remove && self.list(node).as_ref() != Some(&command)) |
| 440 | .map(|node| { |
| 441 | let format = &node.text().unwrap().text.spans()[0].format; |
| 442 | ( |
| 443 | node.id, |
| 444 | list_definition(command == Formatting::Numbering, format), |
| 445 | ) |
| 446 | }) |
| 447 | .collect::<Vec<_>>(); |
| 448 | // A list definition belongs to one paragraph, as OneNote stores it. |
| 449 | let mut lists = BTreeMap::new(); |
| 450 | for (node, definition) in wanted { |
| 451 | let id = new_id()?; |
| 452 | self.definitions.insert(id, definition); |
| 453 | lists.insert(node, id); |
| 454 | } |
| 455 | leaves_mut(&mut replacement, &mut |node| { |
| 456 | if ranges.remove(&node.id).is_some() { |
| 457 | if remove { |
| 458 | node.lists.clear(); |
| 459 | } else if let Some(id) = lists.get(&node.id) { |
| 460 | node.lists = vec![*id]; |
| 461 | } |
| 462 | } |
| 463 | }); |
| 464 | } |
| 465 | Formatting::Tag(tag) => { |
| 466 | let definition = tag.definition(); |
| 467 | let Kind::TagDefinition { |
| 468 | action_type, shape, .. |
| 469 | } = definition.kind |
| 470 | else { |
| 471 | unreachable!() |
| 472 | }; |
| 473 | let existing = self |
| 474 | .definitions |
| 475 | .iter() |
| 476 | .find(|(_, other)| other.kind == definition.kind); |
| 477 | let id = match existing { |
| 478 | Some((id, _)) => *id, |
| 479 | None => { |
| 480 | let id = new_id()?; |
| 481 | self.definitions.insert(id, definition.clone()); |
| 482 | id |
| 483 | } |
| 484 | }; |
| 485 | let has = |node: &PageParagraph| { |
| 486 | tags(node).any(|tag| self.tag_kind(tag) == Some(&definition.kind)) |
| 487 | }; |
| 488 | let remove = covered(&replacement, ends).all(|(node, _)| has(node)); |
| 489 | let created = time32(); |
| 490 | leaves_mut(&mut replacement, &mut |node| { |
| 491 | if ranges.remove(&node.id).is_none() || !remove && has(node) { |
| 492 | return; |
| 493 | } |
| 494 | let ParagraphContent::Text(text) = &mut node.content else { |
| 495 | unreachable!() |
| 496 | }; |
| 497 | // An element holds one tag of each action type. |
| 498 | for tags in [&mut node.tags, &mut text.tags] { |
| 499 | tags.retain(|tag| match self.tag_kind(tag) { |
| 500 | Some(kind) if remove => *kind != definition.kind, |
| 501 | Some(Kind::TagDefinition { |
| 502 | action_type: other, .. |
| 503 | }) => *other != action_type, |
| 504 | _ => true, |
| 505 | }); |
| 506 | } |
| 507 | if !remove { |
| 508 | let checkable = shape == Some(CHECKBOX); |
| 509 | // Stored newest first. |
| 510 | text.tags.insert( |
| 511 | 0, |
| 512 | Tag { |
| 513 | definition: Some(id), |
| 514 | action_type: None, |
| 515 | status: u16::from(!checkable), |
| 516 | created, |
| 517 | completed: if checkable { Some(0) } else { created }, |
| 518 | start: None, |
| 519 | due: None, |
| 520 | task_id: None, |
| 521 | extra_set: 0, |
| 522 | }, |
| 523 | ); |
| 524 | } |
| 525 | }); |
| 526 | } |
| 527 | Formatting::Check => { |
| 528 | let checkable = |tag: &Tag| { |
| 529 | matches!( |
| 530 | self.tag_kind(tag), |
| 531 | Some(Kind::TagDefinition { |
| 532 | shape: Some(CHECKBOX), |
| 533 | .. |
| 534 | }) |
| 535 | ) |
| 536 | }; |
| 537 | let checked = covered(&replacement, ends) |
| 538 | .flat_map(|(node, _)| tags(node)) |
| 539 | .filter(|tag| checkable(tag)) |
| 540 | .all(|tag| tag.status & 1 != 0); |
| 541 | let completed = if checked { Some(0) } else { time32() }; |
| 542 | leaves_mut(&mut replacement, &mut |node| { |
| 543 | if ranges.remove(&node.id).is_none() { |
| 544 | return; |
| 545 | } |
| 546 | let ParagraphContent::Text(text) = &mut node.content else { |
| 547 | unreachable!() |
| 548 | }; |
| 549 | for tag in node.tags.iter_mut().chain(&mut text.tags) { |
| 550 | if checkable(tag) && (tag.status & 1 != 0) == checked { |
| 551 | tag.status ^= 1; |
| 552 | tag.completed = completed; |
| 553 | } |
| 554 | } |
| 555 | }); |
| 556 | } |
| 557 | Formatting::Toggle(_) |
| 558 | | Formatting::Font(_) |
| 559 | | Formatting::FontSize(_) |
| 560 | | Formatting::Color(_) |
| 561 | | Formatting::Highlight(_) |
| 562 | | Formatting::Clear => { |
| 563 | let on = match command { |
| 564 | Formatting::Toggle(toggle) => !character_formats(covered(&replacement, ends)) |
| 565 | .iter() |
| 566 | .all(|format| toggle.get(format) == Some(true)), |
| 567 | _ => true, |
| 568 | }; |
| 569 | let change = |base: &Format, format: &mut Format| match &command { |
| 570 | Formatting::Toggle(toggle) => { |
| 571 | let exclusive = match toggle { |
| 572 | Toggle::Subscript => Some(Toggle::Superscript), |
| 573 | Toggle::Superscript => Some(Toggle::Subscript), |
| 574 | _ => None, |
| 575 | }; |
| 576 | for toggle in exclusive.into_iter().chain([*toggle]) { |
| 577 | *toggle.slot(format) = toggle.get(base).map(|_| false); |
| 578 | } |
| 579 | if on { |
| 580 | *toggle.slot(format) = Some(true); |
| 581 | } |
| 582 | } |
| 583 | Formatting::Font(font) => format.font = Some(font.clone()), |
| 584 | Formatting::FontSize(size) => format.font_size = Some(*size), |
| 585 | Formatting::Color(color) => { |
| 586 | format.color = color.or(base.color.map(|_| AUTOMATIC)); |
| 587 | } |
| 588 | Formatting::Highlight(color) => { |
| 589 | format.highlight = color.or(base.highlight.map(|_| AUTOMATIC)); |
| 590 | } |
| 591 | _ => { |
| 592 | for toggle in Toggle::ALL { |
| 593 | *toggle.slot(format) = toggle.get(base); |
| 594 | } |
| 595 | format.font.clone_from(&base.font); |
| 596 | format.font_size = base.font_size; |
| 597 | format.color = base.color; |
| 598 | format.highlight = base.highlight; |
| 599 | } |
| 600 | }; |
| 601 | let bases = bases(&replacement)?; |
| 602 | if ends[0] == ends[1] { |
| 603 | let [anchor, focus] = selection.positions; |
| 604 | let caret = anchor.min(focus); |
| 605 | let mut format = self.typing_format(caret)?; |
| 606 | change(&bases[&ends[0].0], &mut format); |
| 607 | self.pending = Some((id, caret, format)); |
| 608 | return Ok(()); |
| 609 | } |
| 610 | leaves_mut(&mut replacement, &mut |node| { |
| 611 | if let Some(range) = ranges.remove(&node.id) { |
| 612 | let base = &bases[&node.id]; |
| 613 | let text = &mut node.text_mut().unwrap().text; |
| 614 | *text = restyle(text, range, |format| change(base, format)); |
| 615 | } |
| 616 | }); |
| 617 | } |
| 618 | } |
| 619 | if self.active_outline().document.container(container)?[range.clone()] == replacement[..] { |
| 620 | return Ok(()); |
| 621 | } |
| 622 | self.commit( |
| 623 | engine, |
| 624 | DocumentEdit { |
| 625 | columns: BTreeMap::new(), |
| 626 | container, |
| 627 | range, |
| 628 | replacement, |
| 629 | }, |
| 630 | selection, |
| 631 | ) |
| 632 | } |
| 633 | } |
| 634 | |
| 635 | /// The list OneNote 2010 gives a paragraph with `format`: its Ctrl+. bullet |
| 636 | /// (`corpus/paragraph-edit/reconciliation/keyboard`), or the `##.` arabic numbering it stores |
| 637 | /// from its COM interface (`corpus/outline-edit/tree`). |
| 638 | fn list_definition(numbering: bool, format: &Format) -> Definition { |
| 639 | let font_size = Some(format.font_size.unwrap_or(11.0)); |
| 640 | if numbering { |
| 641 | Definition { |
| 642 | kind: Kind::List { |
| 643 | font: None, |
| 644 | format: Some("\u{fffd}\u{0}.".into()), |
| 645 | restart: None, |
| 646 | bullet: None, |
| 647 | }, |
| 648 | format: Format { |
| 649 | bold: Some(false), |
| 650 | italic: Some(false), |
| 651 | font: format.font.clone(), |
| 652 | font_size, |
| 653 | color: Some(AUTOMATIC), |
| 654 | language: format.language, |
| 655 | ..Format::default() |
| 656 | }, |
| 657 | } |
| 658 | } else { |
| 659 | Definition { |
| 660 | kind: Kind::List { |
| 661 | font: Some("Calibri".into()), |
| 662 | format: Some("\u{2022}".into()), |
| 663 | restart: None, |
| 664 | bullet: Some(1), |
| 665 | }, |
| 666 | format: Format { |
| 667 | font_size, |
| 668 | color: Some(AUTOMATIC), |
| 669 | ..Format::default() |
| 670 | }, |
| 671 | } |
| 672 | } |
| 673 | } |
| 674 | |
| 675 | #[cfg(test)] |
| 676 | mod tests { |
| 677 | use super::*; |
| 678 | |
| 679 | fn at(paragraph: usize, offset: u32) -> TextPosition { |
| 680 | TextPosition { paragraph, offset } |
| 681 | } |
| 682 | |
| 683 | fn calibri() -> Format { |
| 684 | Format { |
| 685 | font: Some("Calibri".into()), |
| 686 | font_size: Some(11.0), |
| 687 | ..Format::default() |
| 688 | } |
| 689 | } |
| 690 | |
| 691 | fn editor(engine: &mut TextEngine, paragraphs: Vec<Paragraph>) -> CanvasEditor { |
| 692 | CanvasEditor::new(engine, TextDocument::new(paragraphs).unwrap(), 300.0).unwrap() |
| 693 | } |
| 694 | |
| 695 | fn plain(engine: &mut TextEngine, texts: &[&str]) -> CanvasEditor { |
| 696 | let paragraphs = texts |
| 697 | .iter() |
| 698 | .map(|text| Paragraph::new((*text).into(), calibri())) |
| 699 | .collect(); |
| 700 | editor(engine, paragraphs) |
| 701 | } |
| 702 | |
| 703 | /// Each paragraph's runs as text and the value `field` reads from their format. |
| 704 | fn runs<T>(editor: &CanvasEditor, field: impl Fn(&Format) -> T) -> Vec<Vec<(String, T)>> { |
| 705 | editor |
| 706 | .active_outline() |
| 707 | .document |
| 708 | .paragraphs() |
| 709 | .map(|paragraph| { |
| 710 | let mut start = 0; |
| 711 | paragraph |
| 712 | .spans() |
| 713 | .iter() |
| 714 | .map(|span| { |
| 715 | let text = paragraph.text()[start..span.end].to_owned(); |
| 716 | start = span.end; |
| 717 | (text, field(&span.format)) |
| 718 | }) |
| 719 | .collect() |
| 720 | }) |
| 721 | .collect() |
| 722 | } |
| 723 | |
| 724 | fn run(text: &str, value: Option<bool>) -> (String, Option<bool>) { |
| 725 | (text.into(), value) |
| 726 | } |
| 727 | |
| 728 | #[test] |
| 729 | fn toggles_apply_unless_every_character_has_them_as_one_undo_step() { |
| 730 | let mut engine = TextEngine::default(); |
| 731 | let mut editor = editor( |
| 732 | &mut engine, |
| 733 | vec![ |
| 734 | Paragraph::from_runs([ |
| 735 | ("plain ".into(), calibri()), |
| 736 | ( |
| 737 | "bold".into(), |
| 738 | Format { |
| 739 | bold: Some(true), |
| 740 | ..calibri() |
| 741 | }, |
| 742 | ), |
| 743 | ]), |
| 744 | Paragraph::new("next".into(), calibri()), |
| 745 | ], |
| 746 | ); |
| 747 | let original = editor.active_outline().document.clone(); |
| 748 | let selection = Selection::from([at(1, 2), at(0, 3)]); |
| 749 | editor.select(selection).unwrap(); |
| 750 | let bold = |editor: &CanvasEditor| runs(editor, |format| format.bold); |
| 751 | editor |
| 752 | .format(&mut engine, Formatting::Toggle(Toggle::Bold)) |
| 753 | .unwrap(); |
| 754 | assert_eq!( |
| 755 | bold(&editor), |
| 756 | [ |
| 757 | vec![run("pla", None), run("in bold", Some(true))], |
| 758 | vec![run("ne", Some(true)), run("xt", None)] |
| 759 | ] |
| 760 | ); |
| 761 | assert_eq!(editor.selection(), selection); |
| 762 | assert!( |
| 763 | editor |
| 764 | .format_state() |
| 765 | .unwrap() |
| 766 | .toggles |
| 767 | .contains(&Toggle::Bold) |
| 768 | ); |
| 769 | editor |
| 770 | .format(&mut engine, Formatting::Toggle(Toggle::Bold)) |
| 771 | .unwrap(); |
| 772 | assert_eq!( |
| 773 | bold(&editor), |
| 774 | [vec![run("plain bold", None)], vec![run("next", None)]] |
| 775 | ); |
| 776 | assert!(editor.undo(&mut engine).unwrap()); |
| 777 | assert!(editor.undo(&mut engine).unwrap()); |
| 778 | assert_eq!(editor.active_outline().document, original); |
| 779 | assert_eq!(editor.selection(), selection); |
| 780 | assert!(!editor.undo(&mut engine).unwrap()); |
| 781 | assert!(editor.redo(&mut engine).unwrap()); |
| 782 | assert_eq!(bold(&editor)[1], [run("ne", Some(true)), run("xt", None)]); |
| 783 | } |
| 784 | |
| 785 | #[test] |
| 786 | fn a_caret_formats_the_text_typed_next_and_scripts_exclude_each_other() { |
| 787 | let mut engine = TextEngine::default(); |
| 788 | let mut editor = plain(&mut engine, &["ab"]); |
| 789 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 790 | for toggle in [Toggle::Bold, Toggle::Superscript, Toggle::Subscript] { |
| 791 | editor |
| 792 | .format(&mut engine, Formatting::Toggle(toggle)) |
| 793 | .unwrap(); |
| 794 | } |
| 795 | assert!(!editor.undo(&mut engine).unwrap()); |
| 796 | assert_eq!( |
| 797 | editor.format_state().unwrap().toggles, |
| 798 | [Toggle::Bold, Toggle::Subscript] |
| 799 | ); |
| 800 | editor.insert(&mut engine, "x").unwrap(); |
| 801 | let typed = Format { |
| 802 | bold: Some(true), |
| 803 | subscript: Some(true), |
| 804 | ..calibri() |
| 805 | }; |
| 806 | assert_eq!( |
| 807 | runs(&editor, Clone::clone), |
| 808 | [vec![ |
| 809 | ("a".into(), calibri()), |
| 810 | ("x".into(), typed.clone()), |
| 811 | ("b".into(), calibri()) |
| 812 | ]] |
| 813 | ); |
| 814 | editor |
| 815 | .format(&mut engine, Formatting::Toggle(Toggle::Italic)) |
| 816 | .unwrap(); |
| 817 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 818 | editor.insert(&mut engine, "y").unwrap(); |
| 819 | assert_eq!(runs(&editor, |format| format.italic)[0][0], run("ya", None)); |
| 820 | |
| 821 | editor |
| 822 | .format(&mut engine, Formatting::FontSize(20.0)) |
| 823 | .unwrap(); |
| 824 | editor.compose(&mut engine, "k".into(), 1..1).unwrap(); |
| 825 | editor.compose(&mut engine, "kl".into(), 2..2).unwrap(); |
| 826 | editor.commit_text(&mut engine, "kl".into()).unwrap(); |
| 827 | assert_eq!( |
| 828 | runs(&editor, |format| format.font_size)[0][..2], |
| 829 | [("y".into(), Some(11.0)), ("kl".into(), Some(20.0))] |
| 830 | ); |
| 831 | editor.undo(&mut engine).unwrap(); |
| 832 | assert_eq!( |
| 833 | editor |
| 834 | .active_outline() |
| 835 | .document |
| 836 | .paragraphs() |
| 837 | .next() |
| 838 | .unwrap() |
| 839 | .text(), |
| 840 | "yaxb" |
| 841 | ); |
| 842 | } |
| 843 | |
| 844 | #[test] |
| 845 | fn clearing_and_removing_attributes_return_to_the_paragraph_style() { |
| 846 | let mut engine = TextEngine::default(); |
| 847 | let style = new_id().unwrap(); |
| 848 | let heading = Format { |
| 849 | bold: Some(true), |
| 850 | color: Some(0x0012_3456), |
| 851 | font: Some("Georgia".into()), |
| 852 | font_size: Some(18.0), |
| 853 | ..Format::default() |
| 854 | }; |
| 855 | let mut nodes = TextDocument::new(vec![Paragraph::new("Heading".into(), heading.clone())]) |
| 856 | .unwrap() |
| 857 | .nodes() |
| 858 | .to_vec(); |
| 859 | nodes[0].style = Some(style); |
| 860 | let outline = Outline { |
| 861 | id: new_id().unwrap(), |
| 862 | title: false, |
| 863 | min_width: None, |
| 864 | layout: onestore::document::Layout { |
| 865 | max_width: Some(300.0), |
| 866 | ..Default::default() |
| 867 | }, |
| 868 | indents: vec![18.0, 0.0, 27.0, 27.0], |
| 869 | paragraphs: nodes, |
| 870 | unsupported: Vec::new(), |
| 871 | }; |
| 872 | let definitions = BTreeMap::from([( |
| 873 | style, |
| 874 | Definition { |
| 875 | kind: Kind::Style { |
| 876 | name: Some("h1".into()), |
| 877 | }, |
| 878 | format: heading.clone(), |
| 879 | }, |
| 880 | )]); |
| 881 | let mut editor = |
| 882 | CanvasEditor::from_outlines(&mut engine, vec![outline], definitions).unwrap(); |
| 883 | editor.select_all().unwrap(); |
| 884 | let state = editor.format_state().unwrap(); |
| 885 | assert_eq!(state.toggles, [Toggle::Bold]); |
| 886 | assert_eq!( |
| 887 | (state.font.as_deref(), state.font_size, state.alignment), |
| 888 | (Some("Georgia"), Some(18.0), Some(Alignment::Left)) |
| 889 | ); |
| 890 | for command in [ |
| 891 | Formatting::Toggle(Toggle::Bold), |
| 892 | Formatting::Color(None), |
| 893 | Formatting::Highlight(Some(0x0000_ffff)), |
| 894 | Formatting::Font("Arial".into()), |
| 895 | Formatting::Align(Alignment::Center), |
| 896 | ] { |
| 897 | editor.format(&mut engine, command).unwrap(); |
| 898 | } |
| 899 | editor.select([at(0, 0), at(0, 4)].into()).unwrap(); |
| 900 | editor |
| 901 | .format(&mut engine, Formatting::FontSize(9.0)) |
| 902 | .unwrap(); |
| 903 | editor.select_all().unwrap(); |
| 904 | assert_eq!(editor.format_state().unwrap().font_size, None); |
| 905 | let format = editor |
| 906 | .active_outline() |
| 907 | .document |
| 908 | .paragraph(0) |
| 909 | .unwrap() |
| 910 | .spans()[1] |
| 911 | .format |
| 912 | .clone(); |
| 913 | assert_eq!( |
| 914 | format, |
| 915 | Format { |
| 916 | bold: Some(false), |
| 917 | color: Some(AUTOMATIC), |
| 918 | highlight: Some(0x0000_ffff), |
| 919 | font: Some("Arial".into()), |
| 920 | alignment: Some(1), |
| 921 | ..heading.clone() |
| 922 | } |
| 923 | ); |
| 924 | editor.format(&mut engine, Formatting::Clear).unwrap(); |
| 925 | assert_eq!( |
| 926 | runs(&editor, Clone::clone), |
| 927 | [vec![( |
| 928 | "Heading".into(), |
| 929 | Format { |
| 930 | alignment: Some(1), |
| 931 | ..heading |
| 932 | } |
| 933 | )]] |
| 934 | ); |
| 935 | editor |
| 936 | .format(&mut engine, Formatting::Highlight(None)) |
| 937 | .unwrap(); |
| 938 | for _ in 0..7 { |
| 939 | assert!(editor.undo(&mut engine).unwrap()); |
| 940 | } |
| 941 | assert!(!editor.undo(&mut engine).unwrap()); |
| 942 | } |
| 943 | |
| 944 | /// Each paragraph's number as laid out, checked against laying the outline out afresh. |
| 945 | fn numbers(engine: &mut TextEngine, editor: &CanvasEditor) -> Vec<Option<u32>> { |
| 946 | let outline = editor.active_outline(); |
| 947 | let fresh = OutlineLayout::flow( |
| 948 | outline.document.nodes().iter(), |
| 949 | &outline.indents, |
| 950 | outline.wrap_width(), |
| 951 | true, |
| 952 | 0, |
| 953 | None, |
| 954 | &mut |node, previous, width, indents| { |
| 955 | ParagraphLayout::shape(engine, node, previous, width, indents, &editor.definitions) |
| 956 | }, |
| 957 | ) |
| 958 | .unwrap(); |
| 959 | let laid = |layout: &OutlineLayout| { |
| 960 | layout |
| 961 | .paragraphs |
| 962 | .iter() |
| 963 | .map(|paragraph| { |
| 964 | let markers = paragraph.markers.iter().map(|(marker, origin)| { |
| 965 | (*origin, marker.lines().next().unwrap().0.metrics().advance) |
| 966 | }); |
| 967 | (paragraph.number, markers.collect::<Vec<_>>()) |
| 968 | }) |
| 969 | .collect::<Vec<_>>() |
| 970 | }; |
| 971 | assert_eq!(laid(&outline.shaped), laid(&fresh)); |
| 972 | outline |
| 973 | .shaped |
| 974 | .paragraphs |
| 975 | .iter() |
| 976 | .map(|paragraph| paragraph.number.map(|(number, _)| number)) |
| 977 | .collect() |
| 978 | } |
| 979 | |
| 980 | #[test] |
| 981 | fn lists_toggle_switch_kind_and_renumber_only_what_changes() { |
| 982 | let mut engine = TextEngine::default(); |
| 983 | let mut editor = plain(&mut engine, &["one", "two", "three", "four"]); |
| 984 | let original = editor.active_outline().document.clone(); |
| 985 | let all = Selection::from([at(0, 0), at(3, 4)]); |
| 986 | editor.select(all).unwrap(); |
| 987 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 988 | let state = editor.format_state().unwrap(); |
| 989 | assert!(state.bullets && !state.numbering); |
| 990 | assert_eq!(numbers(&mut engine, &editor), [None; 4]); |
| 991 | let bullet = editor.active_outline().document.nodes()[0].lists[0]; |
| 992 | assert_eq!( |
| 993 | editor.definitions[&bullet], |
| 994 | list_definition(false, &calibri()) |
| 995 | ); |
| 996 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 997 | let state = editor.format_state().unwrap(); |
| 998 | assert!(!state.bullets && state.numbering); |
| 999 | assert_eq!( |
| 1000 | numbers(&mut engine, &editor), |
| 1001 | [Some(1), Some(2), Some(3), Some(4)] |
| 1002 | ); |
| 1003 | let untouched = editor.active_outline().shaped.paragraphs[0] |
| 1004 | .text |
| 1005 | .shaped |
| 1006 | .styles() |
| 1007 | .as_ptr(); |
| 1008 | editor.select([at(1, 1); 2].into()).unwrap(); |
| 1009 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 1010 | assert_eq!( |
| 1011 | numbers(&mut engine, &editor), |
| 1012 | [Some(1), None, Some(1), Some(2)] |
| 1013 | ); |
| 1014 | assert_eq!( |
| 1015 | editor.active_outline().shaped.paragraphs[0] |
| 1016 | .text |
| 1017 | .shaped |
| 1018 | .styles() |
| 1019 | .as_ptr(), |
| 1020 | untouched |
| 1021 | ); |
| 1022 | editor.undo(&mut engine).unwrap(); |
| 1023 | editor.select([at(2, 0); 2].into()).unwrap(); |
| 1024 | editor.format(&mut engine, Formatting::Indent).unwrap(); |
| 1025 | assert_eq!( |
| 1026 | numbers(&mut engine, &editor), |
| 1027 | [Some(1), Some(2), Some(1), Some(3)] |
| 1028 | ); |
| 1029 | assert_eq!(editor.selection(), Selection::from([at(2, 0); 2])); |
| 1030 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 1031 | assert_eq!( |
| 1032 | numbers(&mut engine, &editor), |
| 1033 | [Some(1), Some(2), None, Some(3)] |
| 1034 | ); |
| 1035 | assert!(editor.format_state().unwrap().bullets); |
| 1036 | editor.format(&mut engine, Formatting::Outdent).unwrap(); |
| 1037 | assert_eq!( |
| 1038 | numbers(&mut engine, &editor), |
| 1039 | [Some(1), Some(2), None, Some(1)] |
| 1040 | ); |
| 1041 | editor.select(all).unwrap(); |
| 1042 | let state = editor.format_state().unwrap(); |
| 1043 | assert!(!state.bullets && !state.numbering); |
| 1044 | for _ in 0..6 { |
| 1045 | editor.undo(&mut engine).unwrap(); |
| 1046 | } |
| 1047 | assert_eq!(editor.active_outline().document, original); |
| 1048 | assert_eq!(numbers(&mut engine, &editor), [None; 4]); |
| 1049 | } |
| 1050 | |
| 1051 | #[test] |
| 1052 | fn numbering_follows_stored_sequences_restarts_and_nesting() { |
| 1053 | let mut engine = TextEngine::default(); |
| 1054 | let mut editor = plain(&mut engine, &["a", "b", "c", "d", "e"]); |
| 1055 | let mut nodes = editor.active_outline().document.nodes().to_vec(); |
| 1056 | for (node, (sequence, restart, level)) in nodes.iter_mut().zip([ |
| 1057 | ('\u{1}', None, 1), |
| 1058 | ('\u{1}', None, 1), |
| 1059 | ('\u{4}', None, 2), |
| 1060 | ('\u{1}', Some(9), 1), |
| 1061 | ('\u{0}', None, 1), |
| 1062 | ]) { |
| 1063 | let id = new_id().unwrap(); |
| 1064 | let mut definition = list_definition(true, &calibri()); |
| 1065 | let Kind::List { |
| 1066 | format, |
| 1067 | restart: value, |
| 1068 | .. |
| 1069 | } = &mut definition.kind |
| 1070 | else { |
| 1071 | unreachable!() |
| 1072 | }; |
| 1073 | *format = Some(format!("(\u{fffd}{sequence})")); |
| 1074 | *value = restart; |
| 1075 | editor.definitions.insert(id, definition); |
| 1076 | node.lists = vec![id]; |
| 1077 | node.level = level; |
| 1078 | } |
| 1079 | let outline = Outline { |
| 1080 | paragraphs: nodes, |
| 1081 | ..editor.active_outline().snapshot() |
| 1082 | }; |
| 1083 | let editor = |
| 1084 | CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap(); |
| 1085 | assert_eq!( |
| 1086 | numbers(&mut engine, &editor), |
| 1087 | [Some(1), Some(2), Some(1), Some(9), Some(10)] |
| 1088 | ); |
| 1089 | } |
| 1090 | |
| 1091 | fn text_tags(editor: &CanvasEditor, paragraph: usize) -> Vec<(String, u16, bool)> { |
| 1092 | let (_, _, node) = editor.active_outline().document.leaf(paragraph).unwrap(); |
| 1093 | tags(node) |
| 1094 | .map(|tag| { |
| 1095 | let Some(Kind::TagDefinition { label, .. }) = editor.tag_kind(tag) else { |
| 1096 | panic!("{tag:?}") |
| 1097 | }; |
| 1098 | let settled = tag.completed |
| 1099 | == if tag.status & 1 == 0 { |
| 1100 | Some(0) |
| 1101 | } else { |
| 1102 | tag.created |
| 1103 | }; |
| 1104 | ( |
| 1105 | label.clone().unwrap(), |
| 1106 | tag.status, |
| 1107 | settled || tag.status & 1 == 1, |
| 1108 | ) |
| 1109 | }) |
| 1110 | .collect() |
| 1111 | } |
| 1112 | |
| 1113 | #[test] |
| 1114 | fn tags_toggle_check_and_replace_tags_of_their_action_type() { |
| 1115 | let mut engine = TextEngine::default(); |
| 1116 | let mut editor = plain(&mut engine, &["task", "other", "music"]); |
| 1117 | let music = new_id().unwrap(); |
| 1118 | editor.definitions.insert( |
| 1119 | music, |
| 1120 | Definition { |
| 1121 | kind: Kind::TagDefinition { |
| 1122 | label: Some("Music".into()), |
| 1123 | action_type: Some(2), |
| 1124 | shape: Some(121), |
| 1125 | color: None, |
| 1126 | highlight: None, |
| 1127 | }, |
| 1128 | format: Format::default(), |
| 1129 | }, |
| 1130 | ); |
| 1131 | let mut nodes = editor.active_outline().document.nodes().to_vec(); |
| 1132 | nodes[2].tags.push(Tag { |
| 1133 | definition: Some(music), |
| 1134 | action_type: None, |
| 1135 | status: 1, |
| 1136 | created: Some(1), |
| 1137 | completed: Some(1), |
| 1138 | start: None, |
| 1139 | due: None, |
| 1140 | task_id: None, |
| 1141 | extra_set: 0, |
| 1142 | }); |
| 1143 | let outline = Outline { |
| 1144 | paragraphs: nodes, |
| 1145 | ..editor.active_outline().snapshot() |
| 1146 | }; |
| 1147 | let mut editor = |
| 1148 | CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap(); |
| 1149 | let original = editor.active_outline().document.clone(); |
| 1150 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 1151 | editor |
| 1152 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) |
| 1153 | .unwrap(); |
| 1154 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]); |
| 1155 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]); |
| 1156 | assert_eq!(editor.format_state().unwrap().tags, [NoteTag::ToDo]); |
| 1157 | let icon = |
| 1158 | |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon; |
| 1159 | assert_eq!( |
| 1160 | icon(&editor), |
| 1161 | crate::outline::TagIcon::CheckBox { checked: false } |
| 1162 | ); |
| 1163 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 1164 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 1165 | assert_eq!( |
| 1166 | icon(&editor), |
| 1167 | crate::outline::TagIcon::CheckBox { checked: true } |
| 1168 | ); |
| 1169 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 1170 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 1171 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 1172 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 1173 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 1, true)]); |
| 1174 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 1175 | |
| 1176 | editor.select([at(1, 0), at(2, 5)].into()).unwrap(); |
| 1177 | editor |
| 1178 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) |
| 1179 | .unwrap(); |
| 1180 | assert_eq!( |
| 1181 | text_tags(&editor, 1), |
| 1182 | [("Question".into(), 1, true), ("To Do".into(), 1, true)] |
| 1183 | ); |
| 1184 | assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]); |
| 1185 | assert_eq!(editor.format_state().unwrap().tags, [NoteTag::Question]); |
| 1186 | let questions = editor |
| 1187 | .definitions |
| 1188 | .values() |
| 1189 | .filter(|definition| **definition == NoteTag::Question.definition()) |
| 1190 | .count(); |
| 1191 | assert_eq!(questions, 1); |
| 1192 | editor |
| 1193 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) |
| 1194 | .unwrap(); |
| 1195 | assert_eq!(text_tags(&editor, 2), []); |
| 1196 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 1197 | editor |
| 1198 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) |
| 1199 | .unwrap(); |
| 1200 | assert_eq!(text_tags(&editor, 0), []); |
| 1201 | for _ in 0..7 { |
| 1202 | editor.undo(&mut engine).unwrap(); |
| 1203 | } |
| 1204 | assert!(!editor.undo(&mut engine).unwrap()); |
| 1205 | assert_eq!(editor.active_outline().document, original); |
| 1206 | } |
| 1207 | |
| 1208 | #[test] |
| 1209 | fn commands_reach_every_cell_a_selection_crosses() { |
| 1210 | let mut engine = TextEngine::default(); |
| 1211 | let mut editor = plain(&mut engine, &["abcdef"]); |
| 1212 | editor.select([at(0, 3); 2].into()).unwrap(); |
| 1213 | editor.tab(&mut engine, false).unwrap(); |
| 1214 | let table = editor.active_outline().document.clone(); |
| 1215 | assert_eq!(table.nodes().len(), 1); |
| 1216 | let selection = Selection::from([at(0, 1), at(1, 2)]); |
| 1217 | editor.select(selection).unwrap(); |
| 1218 | editor |
| 1219 | .format(&mut engine, Formatting::Toggle(Toggle::Italic)) |
| 1220 | .unwrap(); |
| 1221 | assert_eq!( |
| 1222 | runs(&editor, |format| format.italic), |
| 1223 | [ |
| 1224 | vec![run("a", None), run("bc", Some(true))], |
| 1225 | vec![run("de", Some(true)), run("f", None)] |
| 1226 | ] |
| 1227 | ); |
| 1228 | assert!( |
| 1229 | editor |
| 1230 | .format_state() |
| 1231 | .unwrap() |
| 1232 | .toggles |
| 1233 | .contains(&Toggle::Italic) |
| 1234 | ); |
| 1235 | editor |
| 1236 | .format(&mut engine, Formatting::Align(Alignment::Right)) |
| 1237 | .unwrap(); |
| 1238 | assert_eq!( |
| 1239 | editor.format_state().unwrap().alignment, |
| 1240 | Some(Alignment::Right) |
| 1241 | ); |
| 1242 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 1243 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 1244 | assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]); |
| 1245 | editor |
| 1246 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) |
| 1247 | .unwrap(); |
| 1248 | assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1); |
| 1249 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); |
| 1250 | for _ in 0..4 { |
| 1251 | editor.undo(&mut engine).unwrap(); |
| 1252 | } |
| 1253 | assert_eq!(editor.active_outline().document, table); |
| 1254 | assert_eq!(editor.selection(), selection); |
| 1255 | } |
| 1256 | |
| 1257 | #[test] |
| 1258 | fn formatting_lists_and_tags_survive_the_page_writer() { |
| 1259 | use onestore::{RevisionIndex, Store, document::Document}; |
| 1260 | const SECTION: &[u8] = |
| 1261 | include_bytes!("../../../../corpus/paragraph-edit/before/notebook/synthetic.one"); |
| 1262 | let page = |bytes: &[u8]| { |
| 1263 | let store = Store::parse(bytes).unwrap(); |
| 1264 | let index = RevisionIndex::parse(&store).unwrap(); |
| 1265 | let document = Document::parse(&index).unwrap(); |
| 1266 | document |
| 1267 | .pages() |
| 1268 | .unwrap() |
| 1269 | .into_iter() |
| 1270 | .map(|(space, _)| (space, Page::from_space(&document, space).unwrap())) |
| 1271 | .find(|(_, page)| page.title == "Split middle") |
| 1272 | .unwrap() |
| 1273 | }; |
| 1274 | // Tags keep their place in the element's property arena only once read. |
| 1275 | let settled = |mut page: Page| { |
| 1276 | for object in &mut page.objects { |
| 1277 | if let PageObject::Outline(outline) = object { |
| 1278 | for paragraph in &mut outline.paragraphs { |
| 1279 | for tag in &mut paragraph.text_mut().unwrap().tags { |
| 1280 | tag.extra_set = 0; |
| 1281 | } |
| 1282 | } |
| 1283 | } |
| 1284 | } |
| 1285 | page |
| 1286 | }; |
| 1287 | let (space, source) = page(SECTION); |
| 1288 | let mut engine = TextEngine::default(); |
| 1289 | let mut editor = CanvasEditor::from_page(source, &mut engine).unwrap(); |
| 1290 | let body = editor |
| 1291 | .outlines() |
| 1292 | .iter() |
| 1293 | .find(|outline| !outline.title) |
| 1294 | .unwrap() |
| 1295 | .id; |
| 1296 | editor.focus_outline(body).unwrap(); |
| 1297 | let selection = Selection::from([at(0, 2), at(0, 6)]); |
| 1298 | editor.select(selection).unwrap(); |
| 1299 | for command in [ |
| 1300 | Formatting::Toggle(Toggle::Bold), |
| 1301 | Formatting::Toggle(Toggle::Superscript), |
| 1302 | Formatting::FontSize(14.0), |
| 1303 | Formatting::Color(Some(0x0000_00ff)), |
| 1304 | Formatting::Highlight(Some(0x0000_ffff)), |
| 1305 | Formatting::Align(Alignment::Center), |
| 1306 | Formatting::Numbering, |
| 1307 | Formatting::Tag(NoteTag::ToDo), |
| 1308 | Formatting::Tag(NoteTag::Question), |
| 1309 | Formatting::Check, |
| 1310 | ] { |
| 1311 | editor.format(&mut engine, command).unwrap(); |
| 1312 | } |
| 1313 | let state = editor.format_state().unwrap(); |
| 1314 | let edited = settled(editor.page().unwrap()); |
| 1315 | let written = onestore::PreparedEdit::page(SECTION, space, &edited, "Author").unwrap(); |
| 1316 | let (_, reread) = page(written.as_bytes()); |
| 1317 | let reread = settled(reread); |
| 1318 | assert_eq!(reread.objects, edited.objects); |
| 1319 | let mut editor = CanvasEditor::from_page(reread, &mut engine).unwrap(); |
| 1320 | editor.focus_outline(body).unwrap(); |
| 1321 | editor.select(selection).unwrap(); |
| 1322 | assert_eq!(editor.format_state().unwrap(), state); |
| 1323 | assert_eq!( |
| 1324 | state, |
| 1325 | FormatState { |
| 1326 | toggles: vec![Toggle::Bold, Toggle::Superscript], |
| 1327 | font: state.font.clone(), |
| 1328 | font_size: Some(14.0), |
| 1329 | alignment: Some(Alignment::Center), |
| 1330 | bullets: false, |
| 1331 | numbering: true, |
| 1332 | tags: vec![NoteTag::ToDo, NoteTag::Question], |
| 1333 | } |
| 1334 | ); |
| 1335 | } |
| 1336 | } |