| 1 | //! OneNote's Home-ribbon commands: character formatting, lists, alignment and note tags. |
| 2 | |
| 3 | use super::*; |
| 4 | use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE}; |
| 5 | use onestore::document::{Format, Kind, Tag}; |
| 6 | use onestore::page::ParagraphContent; |
| 7 | use onestore::page::text::new_id; |
| 8 | use serde::{Deserialize, Serialize}; |
| 9 | |
| 10 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. |
| 11 | pub(super) const AUTOMATIC: u32 = 0xff00_0000; |
| 12 | /// To Do's check box. |
| 13 | const CHECKBOX: u16 = 3; |
| 14 | |
| 15 | /// A toolbar command on the selection, or with only a caret, on the text typed there next. |
| 16 | #[derive(Clone, Debug, PartialEq)] |
| 17 | pub enum Formatting { |
| 18 | /// Removed where every selected character has it, applied to all of them otherwise. |
| 19 | Toggle(Toggle), |
| 20 | Font(String), |
| 21 | FontSize(f32), |
| 22 | /// A COLORREF, or `None` for automatic. |
| 23 | Color(Option<u32>), |
| 24 | /// A COLORREF, or `None` for no highlight. |
| 25 | Highlight(Option<u32>), |
| 26 | /// Returns text to its paragraph style. |
| 27 | Clear, |
| 28 | /// Toggles OneNote's default bullet or number, as Ctrl+. and Ctrl+/ do. |
| 29 | Bullets, |
| 30 | Numbering, |
| 31 | /// Applies a style from OneNote's bullet or numbering library; `None` removes lists. |
| 32 | List(Option<ListStyle>), |
| 33 | Indent, |
| 34 | Outdent, |
| 35 | Align(Alignment), |
| 36 | /// A tag of the user's list and its place there, which the page stores as its action |
| 37 | /// type. |
| 38 | Tag(NoteTag, u16), |
| 39 | /// Removes every note tag from the selected paragraphs. |
| 40 | RemoveTags, |
| 41 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. |
| 42 | Check, |
| 43 | /// Gives the selected bulleted and numbered paragraphs this tag in place of their list. |
| 44 | ToDoList(NoteTag, u16), |
| 45 | /// Gives the selected paragraphs with this tag OneNote's default bullet in its place. |
| 46 | BulletedList(NoteTag, u16), |
| 47 | /// Format Painter: gives the selection the character formatting and alignment of text |
| 48 | /// picked up with [`CanvasEditor::painted_format`], as OneNote's does. |
| 49 | Paint(Format), |
| 50 | /// Gives the selected paragraphs a paragraph style, as OneNote 2010's Styles gallery |
| 51 | /// does: the page's style of that definition, made where it has none, and their text |
| 52 | /// loses its character formatting but links, fields and language. |
| 53 | Style(Definition), |
| 54 | } |
| 55 | |
| 56 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 57 | pub enum Toggle { |
| 58 | Bold, |
| 59 | Italic, |
| 60 | Underline, |
| 61 | Strikethrough, |
| 62 | /// Excludes superscript. |
| 63 | Subscript, |
| 64 | /// Excludes subscript. |
| 65 | Superscript, |
| 66 | } |
| 67 | |
| 68 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 69 | pub enum Alignment { |
| 70 | Left, |
| 71 | Center, |
| 72 | Right, |
| 73 | } |
| 74 | |
| 75 | /// A style of OneNote 2010's bullet or numbering library, by its place in the gallery. |
| 76 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 77 | pub enum ListStyle { |
| 78 | Bullet(usize), |
| 79 | Number(usize), |
| 80 | } |
| 81 | |
| 82 | /// OneNote 2010's bullet library in gallery order, as it stores each: font, glyph and |
| 83 | /// ListMSAAIndex. Ctrl+. applies the third. |
| 84 | pub const BULLET_LIBRARY: [(&str, &str, u16); 34] = [ |
| 85 | ("Wingdings", "l", 3), |
| 86 | ("Symbol", "\u{b7}", 1), |
| 87 | ("Calibri", "\u{2022}", 1), |
| 88 | ("Courier New", "\u{25cb}", 4), |
| 89 | ("Wingdings 2", "\u{9d}", 6), |
| 90 | ("Wingdings 2", "\u{9c}", 5), |
| 91 | ("Wingdings", "\u{b2}", 13), |
| 92 | ("Tahoma", "\u{25ca}", 14), |
| 93 | ("Wingdings", "w", 15), |
| 94 | ("Wingdings 2", "\u{ae}", 34), |
| 95 | ("Wingdings", "v", 16), |
| 96 | ("Arial", "\u{25aa}", 7), |
| 97 | ("Verdana", "\u{25ab}", 8), |
| 98 | ("Wingdings", "\u{a7}", 9), |
| 99 | ("Arial", "\u{25a1}", 10), |
| 100 | ("Wingdings 3", "}", 11), |
| 101 | ("Arial", "\u{25ba}", 12), |
| 102 | ("Arial", "\u{2192}", 21), |
| 103 | ("Symbol", "\u{de}", 22), |
| 104 | ("Arial", ">", 20), |
| 105 | ("Wingdings", "\u{d8}", 23), |
| 106 | ("Arial", "*", 24), |
| 107 | ("Wingdings", "\u{ad}", 17), |
| 108 | ("Wingdings", "\u{ae}", 19), |
| 109 | ("Wingdings", "\u{af}", 18), |
| 110 | ("Arial", "-", 25), |
| 111 | ("Arial", "\u{2013}", 26), |
| 112 | ("Arial", "\u{2014}", 27), |
| 113 | ("Wingdings", "J", 28), |
| 114 | ("Wingdings", "K", 29), |
| 115 | ("Wingdings", "L", 30), |
| 116 | ("Wingdings", "\u{fc}", 31), |
| 117 | ("Wingdings", "(", 32), |
| 118 | ("Wingdings", "*", 33), |
| 119 | ]; |
| 120 | |
| 121 | /// OneNote 2010's numbering library in gallery order, as NumberListFormat stores each: the |
| 122 | /// number's sequence follows U+FFFD. Ctrl+/ applies the first. |
| 123 | pub const NUMBER_LIBRARY: [&str; 19] = [ |
| 124 | "\u{fffd}\u{0}.", |
| 125 | "\u{fffd}\u{5}.", |
| 126 | "\u{fffd}\u{4}.", |
| 127 | "\u{fffd}\u{3}.", |
| 128 | "\u{fffd}\u{2}.", |
| 129 | "\u{fffd}\u{1}.", |
| 130 | "\u{fffd}\u{0})", |
| 131 | "\u{fffd}\u{4})", |
| 132 | "\u{fffd}\u{3})", |
| 133 | "\u{fffd}\u{2})", |
| 134 | "\u{fffd}\u{1})", |
| 135 | "(\u{fffd}\u{0})", |
| 136 | "(\u{fffd}\u{3})", |
| 137 | "(\u{fffd}\u{4})", |
| 138 | "(\u{fffd}\u{2})", |
| 139 | "(\u{fffd}\u{1})", |
| 140 | "\u{fffd}\u{6}.", |
| 141 | "\u{fffd}\u{7}.", |
| 142 | "\u{fffd}\u{0}-", |
| 143 | ]; |
| 144 | |
| 145 | impl ListStyle { |
| 146 | pub(super) const BULLET: Self = Self::Bullet(2); |
| 147 | pub(super) const NUMBER: Self = Self::Number(0); |
| 148 | |
| 149 | /// The library style a stored list has, if it is one. |
| 150 | pub(super) fn of(kind: &Kind) -> Option<Self> { |
| 151 | let Kind::List { |
| 152 | font, |
| 153 | format: Some(format), |
| 154 | bullet, |
| 155 | .. |
| 156 | } = kind |
| 157 | else { |
| 158 | return None; |
| 159 | }; |
| 160 | match bullet { |
| 161 | Some(bullet) => BULLET_LIBRARY |
| 162 | .iter() |
| 163 | .position(|entry| { |
| 164 | (Some(entry.0), entry.1, entry.2) == (font.as_deref(), format.as_str(), *bullet) |
| 165 | }) |
| 166 | .map(Self::Bullet), |
| 167 | None => NUMBER_LIBRARY |
| 168 | .iter() |
| 169 | .position(|known| known == format) |
| 170 | .map(Self::Number), |
| 171 | } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | /// OneNote's Default font (Options > General): what new outlines' text is set in, as the |
| 176 | /// page's `p` quick style, and the face and colour new titles take. |
| 177 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] |
| 178 | #[serde(default)] |
| 179 | pub struct DefaultFont { |
| 180 | pub face: String, |
| 181 | pub size: f32, |
| 182 | /// COLORREF; none is Automatic. |
| 183 | pub color: Option<u32>, |
| 184 | } |
| 185 | |
| 186 | impl Default for DefaultFont { |
| 187 | /// OneNote 2010's: Calibri 11, Automatic. |
| 188 | fn default() -> Self { |
| 189 | Self { |
| 190 | face: "Calibri".into(), |
| 191 | size: 11.0, |
| 192 | color: None, |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | impl DefaultFont { |
| 198 | /// The `p` quick style OneNote 2010 stores for new text in this font |
| 199 | /// (`corpus/default-font`): plain, in its colour, no highlight or paragraph spacing. |
| 200 | pub(crate) fn body_style(&self) -> Definition { |
| 201 | Definition { |
| 202 | kind: Kind::Style { |
| 203 | name: Some("p".into()), |
| 204 | next: None, |
| 205 | }, |
| 206 | format: Format { |
| 207 | bold: Some(false), |
| 208 | italic: Some(false), |
| 209 | underline: Some(false), |
| 210 | strike: Some(false), |
| 211 | superscript: Some(false), |
| 212 | subscript: Some(false), |
| 213 | font: Some(self.face.clone()), |
| 214 | font_size: Some(self.size), |
| 215 | color: Some(self.color.unwrap_or(AUTOMATIC)), |
| 216 | highlight: Some(AUTOMATIC), |
| 217 | space_before: Some(0.0), |
| 218 | space_after: Some(0.0), |
| 219 | line_spacing: Some(0.0), |
| 220 | ..Format::default() |
| 221 | }, |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | /// A tag of the user's list as the Customize Tags dialog edits it: what its definition |
| 227 | /// stores besides its place in the list, which the page stores as its action type. |
| 228 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 229 | pub struct NoteTag { |
| 230 | pub label: String, |
| 231 | /// MS-ONE's NoteTagShape; 0 has no symbol. |
| 232 | pub shape: u16, |
| 233 | /// COLORREFs; none leaves the text's own. |
| 234 | pub color: Option<u32>, |
| 235 | pub highlight: Option<u32>, |
| 236 | /// Snowbound's own art for the tag, named for its content, which a notebook's |
| 237 | /// `.snowbound` folder maps the tag's name and symbol to; OneNote draws the symbol. |
| 238 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 239 | pub art: Option<String>, |
| 240 | } |
| 241 | |
| 242 | /// OneNote 2010's tag list before it is customized, the first nine on Ctrl+1 to Ctrl+9 |
| 243 | /// (`corpus/structural-probe/tag-gallery.one`): names, symbols and highlights. |
| 244 | const DEFAULT_TAGS: [(&str, u16, Option<u32>); 29] = [ |
| 245 | ("To Do", CHECKBOX, None), |
| 246 | ("Important", 13, None), |
| 247 | ("Question", 15, None), |
| 248 | ("Remember for later", 0, Some(0x0000_ffff)), |
| 249 | ("Definition", 0, Some(0x0000_ff00)), |
| 250 | ("Highlight", 136, None), |
| 251 | ("Contact", 118, None), |
| 252 | ("Address", 23, None), |
| 253 | ("Phone number", 18, None), |
| 254 | ("Web site to visit", 125, None), |
| 255 | ("Idea", 21, None), |
| 256 | ("Password", 131, None), |
| 257 | ("Critical", 17, None), |
| 258 | ("Project A", 100, None), |
| 259 | ("Project B", 101, None), |
| 260 | ("Movie to see", 122, None), |
| 261 | ("Book to read", 132, None), |
| 262 | ("Music to listen to", 121, None), |
| 263 | ("Source for article", 125, None), |
| 264 | ("Remember for blog", 24, None), |
| 265 | ("Discuss with <Person A>", 94, None), |
| 266 | ("Discuss with <Person B>", 94, None), |
| 267 | ("Discuss with manager", 95, None), |
| 268 | ("Send in e-mail", 106, None), |
| 269 | ("Schedule meeting", 12, None), |
| 270 | ("Call back", 12, None), |
| 271 | ("To Do priority 1", 28, None), |
| 272 | ("To Do priority 2", 71, None), |
| 273 | ("Client request", 8, None), |
| 274 | ]; |
| 275 | |
| 276 | impl NoteTag { |
| 277 | pub fn defaults() -> Vec<Self> { |
| 278 | DEFAULT_TAGS |
| 279 | .iter() |
| 280 | .map(|&(label, shape, highlight)| Self { |
| 281 | label: label.into(), |
| 282 | shape, |
| 283 | // The highlighting tags also set black text. |
| 284 | color: highlight.map(|_| 0), |
| 285 | highlight, |
| 286 | art: None, |
| 287 | }) |
| 288 | .collect() |
| 289 | } |
| 290 | |
| 291 | /// The tag as its definition stores it, without Snowbound's art. |
| 292 | pub fn stored(&self) -> Self { |
| 293 | Self { |
| 294 | art: None, |
| 295 | ..self.clone() |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | /// The definition OneNote stores for the tag at place `action_type` in the list. |
| 300 | pub(crate) fn definition(&self, action_type: u16) -> Definition { |
| 301 | Definition { |
| 302 | kind: Kind::TagDefinition { |
| 303 | label: Some(self.label.clone()), |
| 304 | action_type: Some(action_type), |
| 305 | shape: Some(self.shape), |
| 306 | color: self.color, |
| 307 | highlight: self.highlight, |
| 308 | }, |
| 309 | format: Format::default(), |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | /// The tag a stored definition describes, and its action type. |
| 314 | pub fn of(kind: &Kind) -> Option<(Self, u16)> { |
| 315 | let Kind::TagDefinition { |
| 316 | label, |
| 317 | action_type, |
| 318 | shape, |
| 319 | color, |
| 320 | highlight, |
| 321 | } = kind |
| 322 | else { |
| 323 | return None; |
| 324 | }; |
| 325 | Some(( |
| 326 | Self { |
| 327 | label: label.clone().unwrap_or_default(), |
| 328 | shape: shape.unwrap_or(0), |
| 329 | color: *color, |
| 330 | highlight: *highlight, |
| 331 | art: None, |
| 332 | }, |
| 333 | action_type.unwrap_or(0), |
| 334 | )) |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | /// What the selection shows on the toolbar. |
| 339 | #[derive(Clone, Debug, Default, PartialEq)] |
| 340 | pub struct FormatState { |
| 341 | /// Attributes every selected character has. |
| 342 | pub toggles: Vec<Toggle>, |
| 343 | /// The font and size every selected character is laid out in, where they share one. |
| 344 | pub font: Option<String>, |
| 345 | pub font_size: Option<f32>, |
| 346 | /// The alignment every selected paragraph shares. |
| 347 | pub alignment: Option<Alignment>, |
| 348 | /// Whether every selected paragraph is bulleted or numbered. |
| 349 | pub bullets: bool, |
| 350 | pub numbering: bool, |
| 351 | /// Whether any selected paragraph is bulleted or numbered. |
| 352 | pub listed: bool, |
| 353 | /// The library style every selected paragraph's list shares. |
| 354 | pub list: Option<ListStyle>, |
| 355 | /// Tags every selected paragraph has, with their action types. |
| 356 | pub tags: Vec<(NoteTag, u16)>, |
| 357 | /// The stored name of the paragraph style every selected paragraph shares. |
| 358 | pub style: Option<String>, |
| 359 | /// The font and size the selected paragraphs' style gives, or else the Default font, |
| 360 | /// where they share one: what their text is in without formatting of its own. |
| 361 | pub style_font: Option<String>, |
| 362 | pub style_size: Option<f32>, |
| 363 | } |
| 364 | |
| 365 | impl Toggle { |
| 366 | const ALL: [Self; 6] = [ |
| 367 | Self::Bold, |
| 368 | Self::Italic, |
| 369 | Self::Underline, |
| 370 | Self::Strikethrough, |
| 371 | Self::Subscript, |
| 372 | Self::Superscript, |
| 373 | ]; |
| 374 | |
| 375 | fn get(self, format: &Format) -> Option<bool> { |
| 376 | match self { |
| 377 | Self::Bold => format.bold, |
| 378 | Self::Italic => format.italic, |
| 379 | Self::Underline => format.underline, |
| 380 | Self::Strikethrough => format.strike, |
| 381 | Self::Subscript => format.subscript, |
| 382 | Self::Superscript => format.superscript, |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | fn slot(self, format: &mut Format) -> &mut Option<bool> { |
| 387 | match self { |
| 388 | Self::Bold => &mut format.bold, |
| 389 | Self::Italic => &mut format.italic, |
| 390 | Self::Underline => &mut format.underline, |
| 391 | Self::Strikethrough => &mut format.strike, |
| 392 | Self::Subscript => &mut format.subscript, |
| 393 | Self::Superscript => &mut format.superscript, |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | /// Where a selection starts and ends, as text leaf identity and byte offset. |
| 399 | type Ends = [(ExGuid, usize); 2]; |
| 400 | |
| 401 | /// The selection's ends and the container range holding both; a selection ending at a |
| 402 | /// paragraph's start ends with the paragraph before. |
| 403 | pub(super) fn selected( |
| 404 | document: &TextDocument, |
| 405 | selection: Selection, |
| 406 | ) -> Result<(Option<ExGuid>, Range<usize>, Ends), EditError> { |
| 407 | let [anchor, focus] = selection.positions; |
| 408 | let (start, mut end) = (anchor.min(focus), anchor.max(focus)); |
| 409 | if end.offset == 0 && end.paragraph > start.paragraph { |
| 410 | end.paragraph -= 1; |
| 411 | let text = document |
| 412 | .paragraph(end.paragraph) |
| 413 | .ok_or(EditError::InvalidRange)?; |
| 414 | end.offset = text.utf16_offset(text.text().len())?; |
| 415 | } |
| 416 | let (container, first, head) = document |
| 417 | .leaf(start.paragraph) |
| 418 | .ok_or(EditError::InvalidRange)?; |
| 419 | let (end_container, last, tail) = document |
| 420 | .leaf(end.paragraph) |
| 421 | .ok_or(EditError::InvalidRange)?; |
| 422 | let range = if container == end_container { |
| 423 | first..last + 1 |
| 424 | } else { |
| 425 | let root = |container: Option<ExGuid>, index| { |
| 426 | container.map_or(Ok(index), |cell| document.root(cell)) |
| 427 | }; |
| 428 | root(container, first)?..root(end_container, last)? + 1 |
| 429 | }; |
| 430 | let byte = |node: &PageParagraph, offset| node.text().unwrap().text.byte_offset(offset); |
| 431 | Ok(( |
| 432 | if container == end_container { |
| 433 | container |
| 434 | } else { |
| 435 | None |
| 436 | }, |
| 437 | range, |
| 438 | [ |
| 439 | (head.id, byte(head, start.offset)?), |
| 440 | (tail.id, byte(tail, end.offset)?), |
| 441 | ], |
| 442 | )) |
| 443 | } |
| 444 | |
| 445 | /// The text leaves of `nodes` from `start` through `end`, each with the byte range selected. |
| 446 | fn covered( |
| 447 | nodes: &[PageParagraph], |
| 448 | [start, end]: Ends, |
| 449 | ) -> impl Iterator<Item = (&PageParagraph, Range<usize>)> { |
| 450 | let mut done = false; |
| 451 | leaves(nodes, None) |
| 452 | .map(|(_, _, node)| node) |
| 453 | .skip_while(move |node| node.id != start.0) |
| 454 | .take_while(move |node| !std::mem::replace(&mut done, node.id == end.0)) |
| 455 | .map(move |node| { |
| 456 | let from = if node.id == start.0 { start.1 } else { 0 }; |
| 457 | let to = if node.id == end.0 { |
| 458 | end.1 |
| 459 | } else { |
| 460 | node.text().unwrap().text.text().len() |
| 461 | }; |
| 462 | (node, from..to) |
| 463 | }) |
| 464 | } |
| 465 | |
| 466 | pub(super) fn leaves_mut(nodes: &mut [PageParagraph]) -> Vec<&mut PageParagraph> { |
| 467 | let mut leaves = Vec::new(); |
| 468 | for node in nodes { |
| 469 | if node.text().is_some() { |
| 470 | leaves.push(node); |
| 471 | } else if let ParagraphContent::Table(table) = &mut node.content { |
| 472 | for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) { |
| 473 | leaves.extend(leaves_mut(&mut cell.paragraphs)); |
| 474 | } |
| 475 | } |
| 476 | } |
| 477 | leaves |
| 478 | } |
| 479 | |
| 480 | /// `text` with `change` applied to the formats of bytes `range`, or to its only format when empty. |
| 481 | pub(super) fn restyle( |
| 482 | text: &Paragraph, |
| 483 | range: Range<usize>, |
| 484 | change: impl Fn(&mut Format), |
| 485 | ) -> Paragraph { |
| 486 | if text.text().is_empty() { |
| 487 | let mut format = text.spans()[0].format.clone(); |
| 488 | change(&mut format); |
| 489 | return Paragraph::new(String::new(), format); |
| 490 | } |
| 491 | let mut runs = Vec::new(); |
| 492 | let mut start = 0; |
| 493 | for span in text.spans() { |
| 494 | let [from, to] = [range.start, range.end].map(|at| at.clamp(start, span.end)); |
| 495 | for (part, inside) in [ |
| 496 | (start..from, false), |
| 497 | (from..to, true), |
| 498 | (to..span.end, false), |
| 499 | ] { |
| 500 | if !part.is_empty() { |
| 501 | let mut format = span.format.clone(); |
| 502 | if inside { |
| 503 | change(&mut format); |
| 504 | } |
| 505 | runs.push((text.text()[part].to_owned(), format)); |
| 506 | } |
| 507 | } |
| 508 | start = span.end; |
| 509 | } |
| 510 | Paragraph::from_runs(runs) |
| 511 | } |
| 512 | |
| 513 | /// The formats of the visible characters in `range`, or of the paragraphs without text when the |
| 514 | /// selection holds no characters. |
| 515 | fn character_formats<'a>( |
| 516 | covered: impl Iterator<Item = (&'a PageParagraph, Range<usize>)>, |
| 517 | ) -> Vec<&'a Format> { |
| 518 | let mut characters = Vec::new(); |
| 519 | let mut blank = Vec::new(); |
| 520 | for (node, range) in covered { |
| 521 | let text = &node.text().unwrap().text; |
| 522 | if text.text().is_empty() { |
| 523 | blank.push(&text.spans()[0].format); |
| 524 | } |
| 525 | let mut start = 0; |
| 526 | for span in text.spans() { |
| 527 | if start < range.end && span.end > range.start && span.format.hidden != Some(true) { |
| 528 | characters.push(&span.format); |
| 529 | } |
| 530 | start = span.end; |
| 531 | } |
| 532 | } |
| 533 | if characters.is_empty() { |
| 534 | blank |
| 535 | } else { |
| 536 | characters |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | fn common<T: PartialEq>(mut values: impl Iterator<Item = Option<T>>) -> Option<T> { |
| 541 | let first = values.next()??; |
| 542 | values |
| 543 | .all(|value| value.as_ref() == Some(&first)) |
| 544 | .then_some(first) |
| 545 | } |
| 546 | |
| 547 | /// `format` moved from paragraph style `old` to `new`: what the old style gave, or nothing |
| 548 | /// did, the new one gives. |
| 549 | /// `text` in paragraph style `definition` from style format `old`: it loses its character |
| 550 | /// formatting but links, fields and language. |
| 551 | pub(super) fn styled(text: &Paragraph, old: &Format, definition: &Definition) -> Paragraph { |
| 552 | restyle(text, 0..text.text().len(), |format| { |
| 553 | // An equation keeps its own formatting; what it took from the old style it takes |
| 554 | // from the new. |
| 555 | if [format.math, format.embedded_object].contains(&Some(true)) { |
| 556 | *format = followed(format, old, &definition.format); |
| 557 | return; |
| 558 | } |
| 559 | *format = Format { |
| 560 | hidden: format.hidden, |
| 561 | hyperlink: format.hyperlink, |
| 562 | hyperlink_label: format.hyperlink_label, |
| 563 | math: format.math, |
| 564 | embedded_object: format.embedded_object, |
| 565 | language: format.language, |
| 566 | alignment: format.alignment, |
| 567 | rtl: format.rtl, |
| 568 | list_spacing: format.list_spacing, |
| 569 | math_object: format.math_object.clone(), |
| 570 | ..definition.format.clone() |
| 571 | }; |
| 572 | }) |
| 573 | } |
| 574 | |
| 575 | fn followed(format: &Format, old: &Format, new: &Format) -> Format { |
| 576 | let mut format = format.clone(); |
| 577 | macro_rules! follow { |
| 578 | ($($field:ident),*) => {$( |
| 579 | if format.$field.is_none() || old.$field.is_some() && format.$field == old.$field { |
| 580 | format.$field = new.$field.clone(); |
| 581 | } |
| 582 | )*}; |
| 583 | } |
| 584 | follow!( |
| 585 | bold, |
| 586 | italic, |
| 587 | underline, |
| 588 | strike, |
| 589 | superscript, |
| 590 | subscript, |
| 591 | font, |
| 592 | font_size, |
| 593 | color, |
| 594 | highlight, |
| 595 | space_before, |
| 596 | space_after, |
| 597 | line_spacing |
| 598 | ); |
| 599 | format |
| 600 | } |
| 601 | |
| 602 | /// Seconds since 1980, as note tags date themselves. |
| 603 | pub(super) fn time32() -> Option<u32> { |
| 604 | let now = web_time::SystemTime::now() |
| 605 | .duration_since(web_time::UNIX_EPOCH) |
| 606 | .ok()? |
| 607 | .as_secs(); |
| 608 | u32::try_from(now.checked_sub(315_532_800)?).ok() |
| 609 | } |
| 610 | |
| 611 | fn tags(node: &PageParagraph) -> impl Iterator<Item = &Tag> { |
| 612 | node.tags.iter().chain(&node.text().unwrap().tags) |
| 613 | } |
| 614 | |
| 615 | impl CanvasEditor { |
| 616 | /// The format text typed at `position` takes: the caret's pending format, else the text's |
| 617 | /// before it. Text after a link's label or field code is plain, as OneNote types it, and |
| 618 | /// math typed into an equation is a run between its objects until it is built up. |
| 619 | pub(super) fn typing_format(&self, position: TextPosition) -> Result<Format, EditError> { |
| 620 | if let Some((id, at, format)) = &self.pending |
| 621 | && *id == self.active_outline().id |
| 622 | && *at == position |
| 623 | { |
| 624 | return Ok(format.clone()); |
| 625 | } |
| 626 | let text = self |
| 627 | .active_outline() |
| 628 | .document |
| 629 | .paragraph(position.paragraph) |
| 630 | .ok_or(EditError::InvalidRange)?; |
| 631 | let mut format = text.format_at(position.offset)?.clone(); |
| 632 | if format.hidden == Some(true) && format.hyperlink == Some(true) |
| 633 | || super::link::links(text, position.paragraph) |
| 634 | .iter() |
| 635 | .any(|link| link.code.is_some() && link.label.end == position.offset) |
| 636 | { |
| 637 | super::link::unlinked(&mut format); |
| 638 | } |
| 639 | if format.math == Some(true) { |
| 640 | format = onestore::page::Math::format(&format); |
| 641 | } |
| 642 | Ok(format) |
| 643 | } |
| 644 | |
| 645 | fn list(&self, node: &PageParagraph) -> Option<Formatting> { |
| 646 | match &self.definitions.get(node.lists.last()?)?.kind { |
| 647 | Kind::List { format, .. } => Some( |
| 648 | if format |
| 649 | .as_ref() |
| 650 | .is_some_and(|format| format.contains('\u{fffd}')) |
| 651 | { |
| 652 | Formatting::Numbering |
| 653 | } else { |
| 654 | Formatting::Bullets |
| 655 | }, |
| 656 | ), |
| 657 | _ => None, |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | fn list_style(&self, node: &PageParagraph) -> Option<ListStyle> { |
| 662 | ListStyle::of(&self.definitions.get(node.lists.last()?)?.kind) |
| 663 | } |
| 664 | |
| 665 | fn tag_kind(&self, tag: &Tag) -> Option<&Kind<'static>> { |
| 666 | Some(&self.definitions.get(tag.definition.as_ref()?)?.kind) |
| 667 | } |
| 668 | |
| 669 | /// What Format Painter picks up: the first selected character's formatting, or with only |
| 670 | /// a caret, what would be typed there. |
| 671 | pub fn painted_format(&self) -> Result<Format, EditorError> { |
| 672 | let outline = self.active_outline(); |
| 673 | let (container, range, ends) = selected(&outline.document, outline.selection)?; |
| 674 | let [anchor, focus] = outline.selection.positions; |
| 675 | let nodes = &outline.document.container(container)?[range]; |
| 676 | match character_formats(covered(nodes, ends)).first() { |
| 677 | Some(format) if ends[0] != ends[1] => Ok((*format).clone()), |
| 678 | _ => Ok(self.typing_format(anchor.min(focus))?), |
| 679 | } |
| 680 | } |
| 681 | |
| 682 | pub fn format_state(&self) -> Result<FormatState, EditorError> { |
| 683 | let outline = self.active_outline(); |
| 684 | self.format_state_of(outline, outline.selection) |
| 685 | } |
| 686 | |
| 687 | fn format_state_of( |
| 688 | &self, |
| 689 | outline: &TextOutline, |
| 690 | selection: Selection, |
| 691 | ) -> Result<FormatState, EditorError> { |
| 692 | let (container, range, ends) = selected(&outline.document, selection)?; |
| 693 | let nodes = &outline.document.container(container)?[range]; |
| 694 | let paragraphs = covered(nodes, ends) |
| 695 | .map(|(node, _)| node) |
| 696 | .collect::<Vec<_>>(); |
| 697 | let caret; |
| 698 | let formats = if ends[0] == ends[1] { |
| 699 | let [anchor, focus] = selection.positions; |
| 700 | caret = self.typing_format(anchor.min(focus))?; |
| 701 | vec![&caret] |
| 702 | } else { |
| 703 | character_formats(covered(nodes, ends)) |
| 704 | }; |
| 705 | let list = |kind| { |
| 706 | paragraphs |
| 707 | .iter() |
| 708 | .all(|node| self.list(node).as_ref() == Some(&kind)) |
| 709 | }; |
| 710 | let styled = |node: &&PageParagraph| { |
| 711 | (node.style.as_ref()) |
| 712 | .and_then(|style| self.definitions.get(style)) |
| 713 | .map(|definition| &definition.format) |
| 714 | }; |
| 715 | Ok(FormatState { |
| 716 | toggles: Toggle::ALL |
| 717 | .into_iter() |
| 718 | .filter(|toggle| { |
| 719 | formats |
| 720 | .iter() |
| 721 | .all(|format| toggle.get(format) == Some(true)) |
| 722 | }) |
| 723 | .collect(), |
| 724 | font: common( |
| 725 | formats |
| 726 | .iter() |
| 727 | .map(|format| Some(format.font.as_deref().unwrap_or(DEFAULT_FONT).to_owned())), |
| 728 | ), |
| 729 | font_size: common( |
| 730 | formats |
| 731 | .iter() |
| 732 | .map(|format| Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE))), |
| 733 | ), |
| 734 | alignment: common(paragraphs.iter().map(|node| { |
| 735 | Some( |
| 736 | match node.text().unwrap().text.spans()[0].format.alignment { |
| 737 | Some(1) => Alignment::Center, |
| 738 | Some(2) => Alignment::Right, |
| 739 | _ => Alignment::Left, |
| 740 | }, |
| 741 | ) |
| 742 | })), |
| 743 | bullets: list(Formatting::Bullets), |
| 744 | numbering: list(Formatting::Numbering), |
| 745 | listed: paragraphs.iter().any(|node| self.list(node).is_some()), |
| 746 | list: common(paragraphs.iter().map(|node| self.list_style(node))), |
| 747 | tags: paragraphs.first().map_or_else(Vec::new, |first| { |
| 748 | let mut shared: Vec<_> = tags(first) |
| 749 | .filter_map(|tag| NoteTag::of(self.tag_kind(tag)?)) |
| 750 | .filter(|(tag, action_type)| { |
| 751 | let kind = tag.definition(*action_type).kind; |
| 752 | paragraphs |
| 753 | .iter() |
| 754 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) |
| 755 | }) |
| 756 | .collect(); |
| 757 | shared.sort_by_key(|(_, action_type)| *action_type); |
| 758 | shared |
| 759 | }), |
| 760 | style: common(paragraphs.iter().map(|node| { |
| 761 | match &self.definitions.get(&node.style?)?.kind { |
| 762 | Kind::Style { name, .. } => name.clone(), |
| 763 | _ => None, |
| 764 | } |
| 765 | })), |
| 766 | style_font: common(paragraphs.iter().map(|node| { |
| 767 | Some( |
| 768 | styled(node) |
| 769 | .and_then(|format| format.font.clone()) |
| 770 | .unwrap_or_else(|| self.default_font.face.clone()), |
| 771 | ) |
| 772 | })), |
| 773 | style_size: common(paragraphs.iter().map(|node| { |
| 774 | Some( |
| 775 | styled(node) |
| 776 | .and_then(|format| format.font_size) |
| 777 | .unwrap_or(self.default_font.size), |
| 778 | ) |
| 779 | })), |
| 780 | }) |
| 781 | } |
| 782 | |
| 783 | /// Applies a toolbar command as one undo step that keeps the selection. |
| 784 | pub fn format( |
| 785 | &mut self, |
| 786 | engine: &mut TextEngine, |
| 787 | command: Formatting, |
| 788 | ) -> Result<(), EditorError> { |
| 789 | if self.page_selected() { |
| 790 | return self.format_page(engine, command); |
| 791 | } |
| 792 | let outline = self.active_outline(); |
| 793 | let (id, title, selection) = (outline.id, outline.title, outline.selection); |
| 794 | let (container, mut range, ends) = selected(&outline.document, selection)?; |
| 795 | let mut replacement = outline.document.container(container)?[range.clone()].to_vec(); |
| 796 | let mut ranges = covered(&replacement, ends) |
| 797 | .map(|(node, range)| (node.id, range)) |
| 798 | .collect::<BTreeMap<_, _>>(); |
| 799 | let bases = |nodes: &[PageParagraph]| { |
| 800 | covered(nodes, ends) |
| 801 | .map(|(node, _)| Ok((node.id, self.style_format(node.style)?))) |
| 802 | .collect::<Result<BTreeMap<_, _>, EditError>>() |
| 803 | }; |
| 804 | match &command { |
| 805 | Formatting::Align(_) |
| 806 | | Formatting::Bullets |
| 807 | | Formatting::Numbering |
| 808 | | Formatting::List(_) |
| 809 | | Formatting::Tag(..) |
| 810 | | Formatting::RemoveTags |
| 811 | | Formatting::Check |
| 812 | | Formatting::ToDoList(..) |
| 813 | | Formatting::BulletedList(..) |
| 814 | | Formatting::Style(_) |
| 815 | if title => |
| 816 | { |
| 817 | return Ok(()); |
| 818 | } |
| 819 | Formatting::Indent | Formatting::Outdent => { |
| 820 | self.indent(engine, command == Formatting::Outdent)?; |
| 821 | return Ok(()); |
| 822 | } |
| 823 | Formatting::Align(alignment) => { |
| 824 | let bases = bases(&replacement)?; |
| 825 | for node in leaves_mut(&mut replacement) { |
| 826 | if ranges.remove(&node.id).is_some() { |
| 827 | let value = match alignment { |
| 828 | Alignment::Left => bases[&node.id].alignment.map(|_| 0), |
| 829 | Alignment::Center => Some(1), |
| 830 | Alignment::Right => Some(2), |
| 831 | }; |
| 832 | let text = &mut node.text_mut().unwrap().text; |
| 833 | *text = restyle(text, 0..text.text().len(), |format| { |
| 834 | format.alignment = value; |
| 835 | }); |
| 836 | } |
| 837 | } |
| 838 | } |
| 839 | Formatting::Bullets | Formatting::Numbering | Formatting::List(_) => { |
| 840 | let style = match command { |
| 841 | Formatting::Bullets => Some(ListStyle::BULLET), |
| 842 | Formatting::Numbering => Some(ListStyle::NUMBER), |
| 843 | Formatting::List(style) => style, |
| 844 | _ => unreachable!(), |
| 845 | }; |
| 846 | // A toggle leaves paragraphs already of its kind as they are, however styled. |
| 847 | let has = |node: &PageParagraph| match command { |
| 848 | Formatting::List(_) => self.list_style(node) == style, |
| 849 | _ => self.list(node).as_ref() == Some(&command), |
| 850 | }; |
| 851 | let remove = style.is_none() |
| 852 | || !matches!(command, Formatting::List(_)) |
| 853 | && covered(&replacement, ends).all(|(node, _)| has(node)); |
| 854 | // A list applied after a plain sibling nests under it, as Tab would |
| 855 | // (`evidence/structural-edits/xml/pb-1.xml`); removing it, or restyling a |
| 856 | // list, leaves it where it is, as OneNote 2010 does. |
| 857 | let nodes = outline.document.container(container)?; |
| 858 | if !remove |
| 859 | && nodes[range.start].lists.is_empty() |
| 860 | && crate::document::previous_sibling(nodes, range.start, &BTreeSet::new()) |
| 861 | .is_some_and(|sibling| nodes[sibling].lists.is_empty()) |
| 862 | && let Some(edit) = |
| 863 | crate::document::indent(nodes, container, range.clone(), false) |
| 864 | { |
| 865 | range = edit.range; |
| 866 | replacement = edit.replacement; |
| 867 | } |
| 868 | let wanted = covered(&replacement, ends) |
| 869 | .map(|(node, _)| node) |
| 870 | .filter(|node| !remove && !has(node)) |
| 871 | .filter_map(|node| { |
| 872 | let format = &node.text().unwrap().text.spans()[0].format; |
| 873 | Some((node.id, list_definition(style?, format))) |
| 874 | }) |
| 875 | .collect::<Vec<_>>(); |
| 876 | // A list definition belongs to one paragraph, as OneNote stores it. |
| 877 | let mut lists = BTreeMap::new(); |
| 878 | for (node, definition) in wanted { |
| 879 | let id = new_id()?; |
| 880 | self.definitions.insert(id, definition); |
| 881 | lists.insert(node, id); |
| 882 | } |
| 883 | for node in leaves_mut(&mut replacement) { |
| 884 | if ranges.remove(&node.id).is_some() { |
| 885 | if remove { |
| 886 | node.lists.clear(); |
| 887 | } else if let Some(id) = lists.get(&node.id) { |
| 888 | node.lists = vec![*id]; |
| 889 | } |
| 890 | } |
| 891 | } |
| 892 | } |
| 893 | Formatting::Tag(tag, action_type) => { |
| 894 | let definition = tag.definition(*action_type); |
| 895 | let id = self.define_tag(&definition)?; |
| 896 | let has = |node: &PageParagraph| { |
| 897 | tags(node).any(|tag| self.tag_kind(tag) == Some(&definition.kind)) |
| 898 | }; |
| 899 | let remove = covered(&replacement, ends).all(|(node, _)| has(node)); |
| 900 | let created = time32(); |
| 901 | for node in leaves_mut(&mut replacement) { |
| 902 | if ranges.remove(&node.id).is_none() || !remove && has(node) { |
| 903 | continue; |
| 904 | } |
| 905 | let ParagraphContent::Text(text) = &mut node.content else { |
| 906 | unreachable!() |
| 907 | }; |
| 908 | let added = (!remove).then_some((id, tag.shape, created)); |
| 909 | self.retag([&mut node.tags, &mut text.tags], &definition.kind, added); |
| 910 | } |
| 911 | } |
| 912 | Formatting::RemoveTags => { |
| 913 | for node in leaves_mut(&mut replacement) { |
| 914 | if ranges.remove(&node.id).is_some() { |
| 915 | node.tags.clear(); |
| 916 | node.text_mut().unwrap().tags.clear(); |
| 917 | } |
| 918 | } |
| 919 | } |
| 920 | Formatting::Check => self.check(&mut replacement, &ranges), |
| 921 | Formatting::ToDoList(tag, action_type) => { |
| 922 | let definition = tag.definition(*action_type); |
| 923 | let id = self.define_tag(&definition)?; |
| 924 | let created = time32(); |
| 925 | for node in leaves_mut(&mut replacement) { |
| 926 | if ranges.remove(&node.id).is_none() || self.list(node).is_none() { |
| 927 | continue; |
| 928 | } |
| 929 | node.lists.clear(); |
| 930 | if tags(node).any(|tag| self.tag_kind(tag) == Some(&definition.kind)) { |
| 931 | continue; |
| 932 | } |
| 933 | let ParagraphContent::Text(text) = &mut node.content else { |
| 934 | unreachable!() |
| 935 | }; |
| 936 | let added = Some((id, tag.shape, created)); |
| 937 | self.retag([&mut node.tags, &mut text.tags], &definition.kind, added); |
| 938 | } |
| 939 | } |
| 940 | Formatting::BulletedList(tag, action_type) => { |
| 941 | let kind = tag.definition(*action_type).kind; |
| 942 | for node in leaves_mut(&mut replacement) { |
| 943 | if ranges.remove(&node.id).is_none() |
| 944 | || !tags(node).any(|tag| self.tag_kind(tag) == Some(&kind)) |
| 945 | { |
| 946 | continue; |
| 947 | } |
| 948 | if self.list(node) != Some(Formatting::Bullets) { |
| 949 | let format = &node.text().unwrap().text.spans()[0].format; |
| 950 | let list = list_definition(ListStyle::BULLET, format); |
| 951 | let id = new_id()?; |
| 952 | self.definitions.insert(id, list); |
| 953 | node.lists = vec![id]; |
| 954 | } |
| 955 | let ParagraphContent::Text(text) = &mut node.content else { |
| 956 | unreachable!() |
| 957 | }; |
| 958 | self.retag([&mut node.tags, &mut text.tags], &kind, None); |
| 959 | } |
| 960 | } |
| 961 | Formatting::Style(definition) => { |
| 962 | let olds = bases(&replacement)?; |
| 963 | let style = self.define_style(definition)?; |
| 964 | for node in leaves_mut(&mut replacement) { |
| 965 | if ranges.remove(&node.id).is_some() { |
| 966 | node.style = Some(style); |
| 967 | let old = &olds[&node.id]; |
| 968 | let text = &mut node.text_mut().unwrap().text; |
| 969 | *text = styled(text, old, definition); |
| 970 | } |
| 971 | } |
| 972 | } |
| 973 | Formatting::Toggle(_) |
| 974 | | Formatting::Font(_) |
| 975 | | Formatting::FontSize(_) |
| 976 | | Formatting::Color(_) |
| 977 | | Formatting::Highlight(_) |
| 978 | | Formatting::Paint(_) |
| 979 | | Formatting::Clear => { |
| 980 | let caret = (ends[0] == ends[1]) |
| 981 | .then(|| { |
| 982 | let [anchor, focus] = selection.positions; |
| 983 | let caret = anchor.min(focus); |
| 984 | Ok::<_, EditError>((caret, self.typing_format(caret)?)) |
| 985 | }) |
| 986 | .transpose()?; |
| 987 | let on = match command { |
| 988 | Formatting::Toggle(toggle) => !match &caret { |
| 989 | Some((_, format)) => vec![format], |
| 990 | None => character_formats(covered(&replacement, ends)), |
| 991 | } |
| 992 | .iter() |
| 993 | .all(|format| toggle.get(format) == Some(true)), |
| 994 | _ => true, |
| 995 | }; |
| 996 | let change = |base: &Format, format: &mut Format| match &command { |
| 997 | Formatting::Toggle(toggle) => { |
| 998 | let exclusive = match toggle { |
| 999 | Toggle::Subscript => Some(Toggle::Superscript), |
| 1000 | Toggle::Superscript => Some(Toggle::Subscript), |
| 1001 | _ => None, |
| 1002 | }; |
| 1003 | for toggle in exclusive.into_iter().chain([*toggle]) { |
| 1004 | *toggle.slot(format) = toggle.get(base).map(|_| false); |
| 1005 | } |
| 1006 | if on { |
| 1007 | *toggle.slot(format) = Some(true); |
| 1008 | } |
| 1009 | } |
| 1010 | Formatting::Font(font) => format.font = Some(font.clone()), |
| 1011 | Formatting::FontSize(size) => format.font_size = Some(*size), |
| 1012 | Formatting::Color(color) => { |
| 1013 | format.color = color.or(base.color.map(|_| AUTOMATIC)); |
| 1014 | } |
| 1015 | Formatting::Highlight(color) => { |
| 1016 | format.highlight = color.or(base.highlight.map(|_| AUTOMATIC)); |
| 1017 | } |
| 1018 | Formatting::Paint(painted) => { |
| 1019 | for toggle in Toggle::ALL { |
| 1020 | *toggle.slot(format) = toggle.get(painted).or(toggle.get(base)); |
| 1021 | } |
| 1022 | format.font = painted.font.clone().or_else(|| base.font.clone()); |
| 1023 | format.font_size = painted.font_size.or(base.font_size); |
| 1024 | format.color = painted.color.or(base.color); |
| 1025 | format.highlight = painted.highlight.or(base.highlight); |
| 1026 | } |
| 1027 | _ => { |
| 1028 | for toggle in Toggle::ALL { |
| 1029 | *toggle.slot(format) = toggle.get(base); |
| 1030 | } |
| 1031 | format.font.clone_from(&base.font); |
| 1032 | format.font_size = base.font_size; |
| 1033 | format.color = base.color; |
| 1034 | format.highlight = base.highlight; |
| 1035 | } |
| 1036 | }; |
| 1037 | let bases = bases(&replacement)?; |
| 1038 | if let Some((caret, mut format)) = caret { |
| 1039 | change(&bases[&ends[0].0], &mut format); |
| 1040 | self.pending = Some((id, caret, format)); |
| 1041 | return Ok(()); |
| 1042 | } |
| 1043 | for node in leaves_mut(&mut replacement) { |
| 1044 | if let Some(range) = ranges.remove(&node.id) { |
| 1045 | let base = &bases[&node.id]; |
| 1046 | let text = &mut node.text_mut().unwrap().text; |
| 1047 | *text = restyle(text, range, |format| change(base, format)); |
| 1048 | if let Formatting::Paint(painted) = &command |
| 1049 | && !title |
| 1050 | { |
| 1051 | *text = restyle(text, 0..text.text().len(), |format| { |
| 1052 | format.alignment = painted.alignment; |
| 1053 | }); |
| 1054 | } |
| 1055 | } |
| 1056 | } |
| 1057 | } |
| 1058 | } |
| 1059 | if self.active_outline().document.container(container)?[range.clone()] == replacement[..] { |
| 1060 | return Ok(()); |
| 1061 | } |
| 1062 | self.commit( |
| 1063 | engine, |
| 1064 | DocumentEdit { |
| 1065 | columns: BTreeMap::new(), |
| 1066 | container, |
| 1067 | range, |
| 1068 | replacement, |
| 1069 | }, |
| 1070 | selection, |
| 1071 | ) |
| 1072 | } |
| 1073 | } |
| 1074 | |
| 1075 | impl CanvasEditor { |
| 1076 | /// The page's paragraph style holding `definition`, made where it has none. |
| 1077 | pub(super) fn define_style(&mut self, definition: &Definition) -> Result<ExGuid, EditError> { |
| 1078 | if let Some((id, _)) = self |
| 1079 | .definitions |
| 1080 | .iter() |
| 1081 | .find(|(_, kept)| *kept == definition) |
| 1082 | { |
| 1083 | return Ok(*id); |
| 1084 | } |
| 1085 | let id = new_id()?; |
| 1086 | self.definitions.insert(id, definition.clone()); |
| 1087 | Ok(id) |
| 1088 | } |
| 1089 | |
| 1090 | /// The page's definition of the tag `definition` describes, made where it has none. |
| 1091 | pub(super) fn define_tag(&mut self, definition: &Definition) -> Result<ExGuid, EditError> { |
| 1092 | if let Some((id, _)) = self |
| 1093 | .definitions |
| 1094 | .iter() |
| 1095 | .find(|(_, other)| other.kind == definition.kind) |
| 1096 | { |
| 1097 | return Ok(*id); |
| 1098 | } |
| 1099 | let id = new_id()?; |
| 1100 | self.definitions.insert(id, definition.clone()); |
| 1101 | Ok(id) |
| 1102 | } |
| 1103 | |
| 1104 | /// A paragraph style named `name`: the page's in its theme's formatting, else the page's |
| 1105 | /// first of that name, else the theme's made on the page; Normal falls back to the |
| 1106 | /// Default font's. None where nothing names it. |
| 1107 | pub(super) fn style_named(&mut self, name: &str) -> Result<Option<ExGuid>, EditError> { |
| 1108 | let themed = self.styles.get(name).cloned(); |
| 1109 | let named = |definition: &Definition| matches!(&definition.kind, Kind::Style { name: Some(own), .. } if own == name); |
| 1110 | let known = self |
| 1111 | .definitions |
| 1112 | .iter() |
| 1113 | .filter(|(_, kept)| named(kept)) |
| 1114 | .min_by_key(|(_, kept)| Some(*kept) != themed.as_ref()) |
| 1115 | .map(|(id, _)| *id); |
| 1116 | if known.is_some() { |
| 1117 | return Ok(known); |
| 1118 | } |
| 1119 | let definition = themed.or_else(|| (name == "p").then(|| self.default_font.body_style())); |
| 1120 | definition |
| 1121 | .map(|definition| self.define_style(&definition)) |
| 1122 | .transpose() |
| 1123 | } |
| 1124 | } |
| 1125 | |
| 1126 | impl CanvasEditor { |
| 1127 | /// A toolbar command on OneNote 2010's page selection: each outline takes it as if selected |
| 1128 | /// alone, but a toggle turns on everywhere unless every outline already has it. |
| 1129 | fn format_page( |
| 1130 | &mut self, |
| 1131 | engine: &mut TextEngine, |
| 1132 | command: Formatting, |
| 1133 | ) -> Result<(), EditorError> { |
| 1134 | self.whole = None; |
| 1135 | let has = |state: FormatState| match &command { |
| 1136 | Formatting::Toggle(toggle) => state.toggles.contains(toggle), |
| 1137 | Formatting::Bullets => state.bullets, |
| 1138 | Formatting::Numbering => state.numbering, |
| 1139 | Formatting::Tag(tag, action_type) => state.tags.contains(&(tag.stored(), *action_type)), |
| 1140 | _ => false, |
| 1141 | }; |
| 1142 | let mut outlines = Vec::new(); |
| 1143 | for outline in self.outlines.iter().filter(|outline| !outline.title) { |
| 1144 | let state = self.format_state_of(outline, outline.whole())?; |
| 1145 | outlines.push((outline.id, has(state))); |
| 1146 | } |
| 1147 | let everywhere = outlines.iter().all(|(_, has)| *has); |
| 1148 | self.grouped(|editor| { |
| 1149 | for (id, has) in outlines { |
| 1150 | if has && !everywhere { |
| 1151 | continue; |
| 1152 | } |
| 1153 | editor.focus_outline(id)?; |
| 1154 | editor.select_all()?; |
| 1155 | editor.format(engine, command.clone())?; |
| 1156 | } |
| 1157 | Ok(editor.select_page()?) |
| 1158 | }) |
| 1159 | } |
| 1160 | |
| 1161 | /// A click on a check box tag: checks the check boxes of paragraph `id` in outline |
| 1162 | /// `outline`, or clears them once all are checked, as one undo step that keeps the |
| 1163 | /// selection. |
| 1164 | pub fn click_check( |
| 1165 | &mut self, |
| 1166 | engine: &mut TextEngine, |
| 1167 | outline: ExGuid, |
| 1168 | id: ExGuid, |
| 1169 | ) -> Result<(), EditorError> { |
| 1170 | self.focus_outline(outline)?; |
| 1171 | let outline = self.active_outline(); |
| 1172 | let document = &outline.document; |
| 1173 | let Some(paragraph) = leaves(document.nodes(), None).position(|(_, _, node)| node.id == id) |
| 1174 | else { |
| 1175 | return self.click_block_check(engine, id); |
| 1176 | }; |
| 1177 | let at = TextPosition { |
| 1178 | paragraph, |
| 1179 | offset: 0, |
| 1180 | }; |
| 1181 | let (container, range, ends) = selected(document, [at; 2].into())?; |
| 1182 | let mut replacement = document.container(container)?[range.clone()].to_vec(); |
| 1183 | let ranges = covered(&replacement, ends) |
| 1184 | .map(|(node, range)| (node.id, range)) |
| 1185 | .collect(); |
| 1186 | self.check(&mut replacement, &ranges); |
| 1187 | let selection = outline.selection; |
| 1188 | self.commit( |
| 1189 | engine, |
| 1190 | DocumentEdit { |
| 1191 | columns: BTreeMap::new(), |
| 1192 | container, |
| 1193 | range, |
| 1194 | replacement, |
| 1195 | }, |
| 1196 | selection, |
| 1197 | ) |
| 1198 | } |
| 1199 | |
| 1200 | /// A click on the check box of a table, picture or file paragraph `id`. |
| 1201 | fn click_block_check( |
| 1202 | &mut self, |
| 1203 | engine: &mut TextEngine, |
| 1204 | id: ExGuid, |
| 1205 | ) -> Result<(), EditorError> { |
| 1206 | let outline = self.active_outline(); |
| 1207 | let (container, index, node) = descendants(outline.document.nodes(), None) |
| 1208 | .find(|(_, _, node)| node.id == id) |
| 1209 | .ok_or(EditError::InvalidRange)?; |
| 1210 | let mut node = node.clone(); |
| 1211 | let content = match &mut node.content { |
| 1212 | ParagraphContent::Table(table) => table.tags.as_mut_slice(), |
| 1213 | ParagraphContent::Image(image) => image.tags.as_mut_slice(), |
| 1214 | ParagraphContent::Attachment(file) => file.tags.as_mut_slice(), |
| 1215 | _ => &mut [], |
| 1216 | }; |
| 1217 | self.toggle_checks(node.tags.iter_mut().chain(content).collect()); |
| 1218 | let selection = outline.selection; |
| 1219 | self.commit( |
| 1220 | engine, |
| 1221 | DocumentEdit { |
| 1222 | columns: BTreeMap::new(), |
| 1223 | container, |
| 1224 | range: index..index + 1, |
| 1225 | replacement: vec![node], |
| 1226 | }, |
| 1227 | selection, |
| 1228 | ) |
| 1229 | } |
| 1230 | |
| 1231 | /// A tag command on selected picture or file `id`, which OneNote 2010 tags itself rather |
| 1232 | /// than its paragraph (`corpus/object-tags`); false for other commands. |
| 1233 | pub fn format_object( |
| 1234 | &mut self, |
| 1235 | engine: &mut TextEngine, |
| 1236 | id: ExGuid, |
| 1237 | command: &Formatting, |
| 1238 | ) -> Result<bool, EditorError> { |
| 1239 | if !matches!( |
| 1240 | command, |
| 1241 | Formatting::Tag(..) | Formatting::RemoveTags | Formatting::Check |
| 1242 | ) { |
| 1243 | return Ok(false); |
| 1244 | } |
| 1245 | let node = self |
| 1246 | .outline_picture(id) |
| 1247 | .map(|(outline, _, _, node)| (outline, node.clone())); |
| 1248 | let mut tags = match &node { |
| 1249 | Some((_, node)) => match &node.content { |
| 1250 | ParagraphContent::Image(image) => image.tags.clone(), |
| 1251 | ParagraphContent::Attachment(file) => file.tags.clone(), |
| 1252 | _ => return Ok(false), |
| 1253 | }, |
| 1254 | None => self |
| 1255 | .object_tags_mut(id) |
| 1256 | .ok_or(EditError::InvalidRange)? |
| 1257 | .clone(), |
| 1258 | }; |
| 1259 | match command { |
| 1260 | Formatting::Tag(tag, action_type) => { |
| 1261 | let definition = tag.definition(*action_type); |
| 1262 | let defined = self.define_tag(&definition)?; |
| 1263 | let has = tags |
| 1264 | .iter() |
| 1265 | .any(|tag| self.tag_kind(tag) == Some(&definition.kind)); |
| 1266 | let added = (!has).then_some((defined, tag.shape, time32())); |
| 1267 | self.retag([&mut tags], &definition.kind, added); |
| 1268 | } |
| 1269 | Formatting::RemoveTags => tags.clear(), |
| 1270 | _ => self.toggle_checks(tags.iter_mut().collect()), |
| 1271 | } |
| 1272 | self.finish_composition(); |
| 1273 | let Some((outline, mut node)) = node else { |
| 1274 | self.set_object_tags(id, tags); |
| 1275 | return Ok(true); |
| 1276 | }; |
| 1277 | match &mut node.content { |
| 1278 | ParagraphContent::Image(image) => image.tags = tags, |
| 1279 | ParagraphContent::Attachment(file) => file.tags = tags, |
| 1280 | _ => unreachable!(), |
| 1281 | } |
| 1282 | self.focus_outline(outline)?; |
| 1283 | let (container, index, _) = descendants(self.active_outline().document.nodes(), None) |
| 1284 | .find(|(_, _, other)| other.id == node.id) |
| 1285 | .ok_or(EditError::InvalidRange)?; |
| 1286 | let selection = self.active_outline().selection; |
| 1287 | self.commit( |
| 1288 | engine, |
| 1289 | DocumentEdit { |
| 1290 | columns: BTreeMap::new(), |
| 1291 | container, |
| 1292 | range: index..index + 1, |
| 1293 | replacement: vec![node], |
| 1294 | }, |
| 1295 | selection, |
| 1296 | )?; |
| 1297 | Ok(true) |
| 1298 | } |
| 1299 | |
| 1300 | /// A click on the check box of picture or file `id` on the page. |
| 1301 | pub fn click_object_check(&mut self, id: ExGuid) -> Result<(), EditorError> { |
| 1302 | let mut tags = self |
| 1303 | .objects |
| 1304 | .iter() |
| 1305 | .find_map(|object| object.tagged().filter(|tagged| tagged.0 == id)) |
| 1306 | .ok_or(EditError::InvalidRange)? |
| 1307 | .1 |
| 1308 | .to_vec(); |
| 1309 | self.toggle_checks(tags.iter_mut().collect()); |
| 1310 | self.finish_composition(); |
| 1311 | self.set_object_tags(id, tags); |
| 1312 | Ok(()) |
| 1313 | } |
| 1314 | |
| 1315 | /// Takes tags of `kind` from `lists`, or with `added`, the definition's identity, its |
| 1316 | /// shape and when, gives the last list one in place of any of its action type; an element |
| 1317 | /// holds one tag of each action type, stored newest first. |
| 1318 | pub(super) fn retag<const N: usize>( |
| 1319 | &self, |
| 1320 | mut lists: [&mut Vec<Tag>; N], |
| 1321 | kind: &Kind<'static>, |
| 1322 | added: Option<(ExGuid, u16, Option<u32>)>, |
| 1323 | ) { |
| 1324 | let Kind::TagDefinition { action_type, .. } = kind else { |
| 1325 | return; |
| 1326 | }; |
| 1327 | for tags in lists.iter_mut() { |
| 1328 | tags.retain(|tag| match self.tag_kind(tag) { |
| 1329 | Some(other) if added.is_none() => other != kind, |
| 1330 | Some(Kind::TagDefinition { |
| 1331 | action_type: other, .. |
| 1332 | }) => other != action_type, |
| 1333 | _ => true, |
| 1334 | }); |
| 1335 | } |
| 1336 | let (Some((id, shape, created)), Some(tags)) = (added, lists.last_mut()) else { |
| 1337 | return; |
| 1338 | }; |
| 1339 | let checkable = crate::outline::checkable(shape); |
| 1340 | tags.insert( |
| 1341 | 0, |
| 1342 | Tag { |
| 1343 | definition: Some(id), |
| 1344 | action_type: None, |
| 1345 | shape: None, |
| 1346 | property_status: None, |
| 1347 | status: u16::from(!checkable), |
| 1348 | created, |
| 1349 | completed: if checkable { Some(0) } else { created }, |
| 1350 | start: None, |
| 1351 | due: None, |
| 1352 | task_id: None, |
| 1353 | extra_set: 0, |
| 1354 | }, |
| 1355 | ); |
| 1356 | } |
| 1357 | |
| 1358 | /// Checks the check boxes among `tags`, or clears them once all are checked; OneNote keeps |
| 1359 | /// a cleared box's completion time as zero. |
| 1360 | pub(super) fn toggle_checks(&self, mut tags: Vec<&mut Tag>) { |
| 1361 | let checked = tags |
| 1362 | .iter() |
| 1363 | .filter(|tag| self.check_box(tag).is_some()) |
| 1364 | .all(|tag| tag.status & 1 != 0); |
| 1365 | let completed = if checked { Some(0) } else { time32() }; |
| 1366 | for tag in &mut tags { |
| 1367 | if self.check_box(tag).is_some() && (tag.status & 1 != 0) == checked { |
| 1368 | tag.status ^= 1; |
| 1369 | tag.completed = completed; |
| 1370 | } |
| 1371 | } |
| 1372 | } |
| 1373 | |
| 1374 | /// The definition and shape of `tag` when it is a check box. |
| 1375 | fn check_box(&self, tag: &Tag) -> Option<(&Kind<'static>, u16)> { |
| 1376 | match self.tag_kind(tag)? { |
| 1377 | kind @ Kind::TagDefinition { |
| 1378 | shape: Some(shape), .. |
| 1379 | } if crate::outline::checkable(*shape) => Some((kind, *shape)), |
| 1380 | _ => None, |
| 1381 | } |
| 1382 | } |
| 1383 | |
| 1384 | /// Enter in a to-do list: `opened` takes an unchecked copy of each check box of `from`. |
| 1385 | pub(super) fn continue_checks(&self, from: &PageParagraph, opened: &mut PageParagraph) { |
| 1386 | let created = time32(); |
| 1387 | let ParagraphContent::Text(text) = &mut opened.content else { |
| 1388 | unreachable!() |
| 1389 | }; |
| 1390 | for tag in tags(from).collect::<Vec<_>>().into_iter().rev() { |
| 1391 | if let (Some((kind, shape)), Some(id)) = (self.check_box(tag), tag.definition) { |
| 1392 | self.retag( |
| 1393 | [&mut opened.tags, &mut text.tags], |
| 1394 | kind, |
| 1395 | Some((id, shape, created)), |
| 1396 | ); |
| 1397 | } |
| 1398 | } |
| 1399 | } |
| 1400 | |
| 1401 | /// Enter on an empty to-do item ends the list: its check boxes go. |
| 1402 | pub(super) fn end_checks(&self, node: &mut PageParagraph) { |
| 1403 | let ParagraphContent::Text(text) = &mut node.content else { |
| 1404 | unreachable!() |
| 1405 | }; |
| 1406 | for tags in [&mut node.tags, &mut text.tags] { |
| 1407 | tags.retain(|tag| self.check_box(tag).is_none()); |
| 1408 | } |
| 1409 | } |
| 1410 | |
| 1411 | /// Checks the check boxes of the `covered` paragraphs, or clears them once all are |
| 1412 | /// checked. |
| 1413 | fn check(&self, replacement: &mut [PageParagraph], covered: &BTreeMap<ExGuid, Range<usize>>) { |
| 1414 | let tags = leaves_mut(replacement) |
| 1415 | .into_iter() |
| 1416 | .filter(|node| covered.contains_key(&node.id)) |
| 1417 | .flat_map(|node| { |
| 1418 | let ParagraphContent::Text(text) = &mut node.content else { |
| 1419 | unreachable!() |
| 1420 | }; |
| 1421 | node.tags.iter_mut().chain(&mut text.tags) |
| 1422 | }) |
| 1423 | .collect(); |
| 1424 | self.toggle_checks(tags); |
| 1425 | } |
| 1426 | } |
| 1427 | |
| 1428 | /// The list a Tab (`deeper`) or Shift+Tab gives a paragraph whose list is one OneNote 2010 |
| 1429 | /// steps through as Tab nests it: from Ctrl+. through nine bullets, and from Ctrl+/ through |
| 1430 | /// eight number formats, each as its gallery stores it, starting over after the last |
| 1431 | /// (`evidence/toolbar-17/tab-lists.txt`). Outdenting stops at the first. |
| 1432 | pub(super) fn nested_list(definition: &Definition, deeper: bool) -> Option<Definition> { |
| 1433 | const BULLETS: [usize; 9] = [2, 3, 13, 14, 9, 7, 15, 26, 8]; |
| 1434 | const NUMBERS: [usize; 8] = [0, 2, 4, 6, 7, 9, 16, 17]; |
| 1435 | let Kind::List { restart, .. } = &definition.kind else { |
| 1436 | return None; |
| 1437 | }; |
| 1438 | let current = ListStyle::of(&definition.kind)?; |
| 1439 | let (chain, style): (&[usize], fn(usize) -> ListStyle) = match current { |
| 1440 | ListStyle::Bullet(_) => (&BULLETS, ListStyle::Bullet), |
| 1441 | ListStyle::Number(_) => (&NUMBERS, ListStyle::Number), |
| 1442 | }; |
| 1443 | let at = chain.iter().position(|place| style(*place) == current)?; |
| 1444 | let at = if deeper { |
| 1445 | (at + 1) % chain.len() |
| 1446 | } else { |
| 1447 | at.checked_sub(1)? |
| 1448 | }; |
| 1449 | let mut kind = list_definition(style(chain[at]), &Format::default()).kind; |
| 1450 | if let Kind::List { restart: value, .. } = &mut kind { |
| 1451 | *value = *restart; |
| 1452 | } |
| 1453 | Some(Definition { |
| 1454 | kind, |
| 1455 | format: definition.format.clone(), |
| 1456 | }) |
| 1457 | } |
| 1458 | |
| 1459 | /// The list OneNote 2010 gives a paragraph with `format` in library `style`, as its gallery |
| 1460 | /// and its Ctrl+. bullet store it (`corpus/paragraph-edit/reconciliation/keyboard`); a number |
| 1461 | /// also takes the text's font and language (`corpus/outline-edit/tree`). |
| 1462 | pub(super) fn list_definition(style: ListStyle, format: &Format) -> Definition { |
| 1463 | let font_size = Some(format.font_size.unwrap_or(DEFAULT_FONT_SIZE)); |
| 1464 | match style { |
| 1465 | ListStyle::Number(index) => Definition { |
| 1466 | kind: Kind::List { |
| 1467 | font: None, |
| 1468 | format: Some(NUMBER_LIBRARY[index].into()), |
| 1469 | restart: None, |
| 1470 | bullet: None, |
| 1471 | }, |
| 1472 | format: Format { |
| 1473 | bold: Some(false), |
| 1474 | italic: Some(false), |
| 1475 | font: format.font.clone(), |
| 1476 | font_size, |
| 1477 | color: Some(AUTOMATIC), |
| 1478 | language: format.language, |
| 1479 | ..Format::default() |
| 1480 | }, |
| 1481 | }, |
| 1482 | ListStyle::Bullet(index) => { |
| 1483 | let (font, glyph, bullet) = BULLET_LIBRARY[index]; |
| 1484 | Definition { |
| 1485 | kind: Kind::List { |
| 1486 | font: Some(font.into()), |
| 1487 | format: Some(glyph.into()), |
| 1488 | restart: None, |
| 1489 | bullet: Some(bullet), |
| 1490 | }, |
| 1491 | format: Format { |
| 1492 | font_size, |
| 1493 | color: Some(AUTOMATIC), |
| 1494 | ..Format::default() |
| 1495 | }, |
| 1496 | } |
| 1497 | } |
| 1498 | } |
| 1499 | } |
| 1500 | |
| 1501 | #[cfg(test)] |
| 1502 | mod tests { |
| 1503 | use super::*; |
| 1504 | |
| 1505 | fn at(paragraph: usize, offset: u32) -> TextPosition { |
| 1506 | TextPosition { paragraph, offset } |
| 1507 | } |
| 1508 | |
| 1509 | fn calibri() -> Format { |
| 1510 | Format { |
| 1511 | font: Some("Calibri".into()), |
| 1512 | font_size: Some(11.0), |
| 1513 | ..Format::default() |
| 1514 | } |
| 1515 | } |
| 1516 | |
| 1517 | fn editor(engine: &mut TextEngine, paragraphs: Vec<Paragraph>) -> CanvasEditor { |
| 1518 | CanvasEditor::new(engine, TextDocument::new(paragraphs).unwrap(), 300.0).unwrap() |
| 1519 | } |
| 1520 | |
| 1521 | fn plain(engine: &mut TextEngine, texts: &[&str]) -> CanvasEditor { |
| 1522 | let paragraphs = texts |
| 1523 | .iter() |
| 1524 | .map(|text| Paragraph::new((*text).into(), calibri())) |
| 1525 | .collect(); |
| 1526 | editor(engine, paragraphs) |
| 1527 | } |
| 1528 | |
| 1529 | /// Each paragraph's runs as text and the value `field` reads from their format. |
| 1530 | fn runs<T>(editor: &CanvasEditor, field: impl Fn(&Format) -> T) -> Vec<Vec<(String, T)>> { |
| 1531 | editor |
| 1532 | .active_outline() |
| 1533 | .document |
| 1534 | .paragraphs() |
| 1535 | .map(|paragraph| { |
| 1536 | let mut start = 0; |
| 1537 | paragraph |
| 1538 | .spans() |
| 1539 | .iter() |
| 1540 | .map(|span| { |
| 1541 | let text = paragraph.text()[start..span.end].to_owned(); |
| 1542 | start = span.end; |
| 1543 | (text, field(&span.format)) |
| 1544 | }) |
| 1545 | .collect() |
| 1546 | }) |
| 1547 | .collect() |
| 1548 | } |
| 1549 | |
| 1550 | fn run(text: &str, value: Option<bool>) -> (String, Option<bool>) { |
| 1551 | (text.into(), value) |
| 1552 | } |
| 1553 | |
| 1554 | #[test] |
| 1555 | fn toggles_apply_unless_every_character_has_them_as_one_undo_step() { |
| 1556 | let mut engine = TextEngine::default(); |
| 1557 | let mut editor = editor( |
| 1558 | &mut engine, |
| 1559 | vec![ |
| 1560 | Paragraph::from_runs([ |
| 1561 | ("plain ".into(), calibri()), |
| 1562 | ( |
| 1563 | "bold".into(), |
| 1564 | Format { |
| 1565 | bold: Some(true), |
| 1566 | ..calibri() |
| 1567 | }, |
| 1568 | ), |
| 1569 | ]), |
| 1570 | Paragraph::new("next".into(), calibri()), |
| 1571 | ], |
| 1572 | ); |
| 1573 | let original = editor.active_outline().document.clone(); |
| 1574 | let selection = Selection::from([at(1, 2), at(0, 3)]); |
| 1575 | editor.select(selection).unwrap(); |
| 1576 | let bold = |editor: &CanvasEditor| runs(editor, |format| format.bold); |
| 1577 | editor |
| 1578 | .format(&mut engine, Formatting::Toggle(Toggle::Bold)) |
| 1579 | .unwrap(); |
| 1580 | assert_eq!( |
| 1581 | bold(&editor), |
| 1582 | [ |
| 1583 | vec![run("pla", None), run("in bold", Some(true))], |
| 1584 | vec![run("ne", Some(true)), run("xt", None)] |
| 1585 | ] |
| 1586 | ); |
| 1587 | assert_eq!(editor.selection(), selection); |
| 1588 | assert!( |
| 1589 | editor |
| 1590 | .format_state() |
| 1591 | .unwrap() |
| 1592 | .toggles |
| 1593 | .contains(&Toggle::Bold) |
| 1594 | ); |
| 1595 | editor |
| 1596 | .format(&mut engine, Formatting::Toggle(Toggle::Bold)) |
| 1597 | .unwrap(); |
| 1598 | assert_eq!( |
| 1599 | bold(&editor), |
| 1600 | [vec![run("plain bold", None)], vec![run("next", None)]] |
| 1601 | ); |
| 1602 | assert!(editor.undo(&mut engine).unwrap()); |
| 1603 | assert!(editor.undo(&mut engine).unwrap()); |
| 1604 | assert_eq!(editor.active_outline().document, original); |
| 1605 | assert_eq!(editor.selection(), selection); |
| 1606 | assert!(!editor.undo(&mut engine).unwrap()); |
| 1607 | assert!(editor.redo(&mut engine).unwrap()); |
| 1608 | assert_eq!(bold(&editor)[1], [run("ne", Some(true)), run("xt", None)]); |
| 1609 | } |
| 1610 | |
| 1611 | #[test] |
| 1612 | fn a_second_toggle_at_a_caret_turns_the_typing_format_back_off() { |
| 1613 | for toggle in Toggle::ALL { |
| 1614 | for text in ["", "ab"] { |
| 1615 | let mut engine = TextEngine::default(); |
| 1616 | let mut editor = plain(&mut engine, &[text]); |
| 1617 | let caret = at(0, text.len() as u32); |
| 1618 | editor.select([caret; 2].into()).unwrap(); |
| 1619 | let press = |editor: &mut CanvasEditor, engine: &mut TextEngine| { |
| 1620 | editor.format(engine, Formatting::Toggle(toggle)).unwrap(); |
| 1621 | editor.format_state().unwrap().toggles |
| 1622 | }; |
| 1623 | assert_eq!(press(&mut editor, &mut engine), [toggle], "{toggle:?}"); |
| 1624 | assert_eq!(press(&mut editor, &mut engine), [], "{toggle:?}"); |
| 1625 | editor.insert(&mut engine, "x").unwrap(); |
| 1626 | assert_eq!( |
| 1627 | runs(&editor, |format| toggle.get(format))[0], |
| 1628 | [run(&format!("{text}x"), None)] |
| 1629 | ); |
| 1630 | } |
| 1631 | let mut engine = TextEngine::default(); |
| 1632 | let mut on = calibri(); |
| 1633 | *toggle.slot(&mut on) = Some(true); |
| 1634 | let mut editor = editor(&mut engine, vec![Paragraph::new("ab".into(), on)]); |
| 1635 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 1636 | editor |
| 1637 | .format(&mut engine, Formatting::Toggle(toggle)) |
| 1638 | .unwrap(); |
| 1639 | assert_eq!(editor.format_state().unwrap().toggles, [], "{toggle:?}"); |
| 1640 | editor.insert(&mut engine, "x").unwrap(); |
| 1641 | assert_eq!( |
| 1642 | runs(&editor, |format| toggle.get(format) == Some(true))[0], |
| 1643 | [("a".into(), true), ("x".into(), false), ("b".into(), true)], |
| 1644 | "{toggle:?}" |
| 1645 | ); |
| 1646 | } |
| 1647 | } |
| 1648 | |
| 1649 | #[test] |
| 1650 | fn a_caret_formats_the_text_typed_next_and_scripts_exclude_each_other() { |
| 1651 | let mut engine = TextEngine::default(); |
| 1652 | let mut editor = plain(&mut engine, &["ab"]); |
| 1653 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 1654 | for toggle in [Toggle::Bold, Toggle::Superscript, Toggle::Subscript] { |
| 1655 | editor |
| 1656 | .format(&mut engine, Formatting::Toggle(toggle)) |
| 1657 | .unwrap(); |
| 1658 | } |
| 1659 | assert!(!editor.undo(&mut engine).unwrap()); |
| 1660 | assert_eq!( |
| 1661 | editor.format_state().unwrap().toggles, |
| 1662 | [Toggle::Bold, Toggle::Subscript] |
| 1663 | ); |
| 1664 | editor.insert(&mut engine, "x").unwrap(); |
| 1665 | let typed = Format { |
| 1666 | bold: Some(true), |
| 1667 | subscript: Some(true), |
| 1668 | ..calibri() |
| 1669 | }; |
| 1670 | assert_eq!( |
| 1671 | runs(&editor, Clone::clone), |
| 1672 | [vec![ |
| 1673 | ("a".into(), calibri()), |
| 1674 | ("x".into(), typed.clone()), |
| 1675 | ("b".into(), calibri()) |
| 1676 | ]] |
| 1677 | ); |
| 1678 | editor |
| 1679 | .format(&mut engine, Formatting::Toggle(Toggle::Italic)) |
| 1680 | .unwrap(); |
| 1681 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 1682 | editor.insert(&mut engine, "y").unwrap(); |
| 1683 | assert_eq!(runs(&editor, |format| format.italic)[0][0], run("ya", None)); |
| 1684 | |
| 1685 | editor |
| 1686 | .format(&mut engine, Formatting::FontSize(20.0)) |
| 1687 | .unwrap(); |
| 1688 | editor.compose(&mut engine, "k".into(), 1..1).unwrap(); |
| 1689 | editor.compose(&mut engine, "kl".into(), 2..2).unwrap(); |
| 1690 | editor.commit_text(&mut engine, "kl".into()).unwrap(); |
| 1691 | assert_eq!( |
| 1692 | runs(&editor, |format| format.font_size)[0][..2], |
| 1693 | [("y".into(), Some(11.0)), ("kl".into(), Some(20.0))] |
| 1694 | ); |
| 1695 | editor.undo(&mut engine).unwrap(); |
| 1696 | assert_eq!( |
| 1697 | editor |
| 1698 | .active_outline() |
| 1699 | .document |
| 1700 | .paragraphs() |
| 1701 | .next() |
| 1702 | .unwrap() |
| 1703 | .text(), |
| 1704 | "yaxb" |
| 1705 | ); |
| 1706 | } |
| 1707 | |
| 1708 | /// A style from the gallery replaces the paragraph's character formatting but keeps its |
| 1709 | /// language, as OneNote 2010's does; Enter at its end takes its NextStyle, from the |
| 1710 | /// theme where the page has none (lab, 2026-09-30). |
| 1711 | #[test] |
| 1712 | fn a_gallery_style_clears_formatting_and_enter_takes_its_next_style() { |
| 1713 | let mut engine = TextEngine::default(); |
| 1714 | let body = Format { |
| 1715 | font: Some("Calibri".into()), |
| 1716 | font_size: Some(11.0), |
| 1717 | language: Some(0x40c), |
| 1718 | ..Format::default() |
| 1719 | }; |
| 1720 | let source = TextDocument::new(vec![Paragraph::from_runs([ |
| 1721 | ("Plain ".to_owned(), body.clone()), |
| 1722 | ( |
| 1723 | "red".to_owned(), |
| 1724 | Format { |
| 1725 | color: Some(0xff), |
| 1726 | italic: Some(true), |
| 1727 | ..body.clone() |
| 1728 | }, |
| 1729 | ), |
| 1730 | ])]) |
| 1731 | .unwrap(); |
| 1732 | let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap(); |
| 1733 | let style = |name: &str, size, next: Option<&str>| Definition { |
| 1734 | kind: Kind::Style { |
| 1735 | name: Some(name.into()), |
| 1736 | next: next.map(Into::into), |
| 1737 | }, |
| 1738 | format: Format { |
| 1739 | bold: Some(name != "p"), |
| 1740 | italic: Some(false), |
| 1741 | font: Some("Georgia".into()), |
| 1742 | font_size: Some(size), |
| 1743 | color: Some(0x0033_2211), |
| 1744 | space_before: Some(12.0), |
| 1745 | ..Format::default() |
| 1746 | }, |
| 1747 | }; |
| 1748 | let heading = style("h2", 16.0, Some("p")); |
| 1749 | editor.styles = BTreeMap::from([("p".to_owned(), style("p", 12.0, None))]); |
| 1750 | editor.select([at(0, 3); 2].into()).unwrap(); |
| 1751 | editor |
| 1752 | .format(&mut engine, Formatting::Style(heading.clone())) |
| 1753 | .unwrap(); |
| 1754 | let node = editor.active_outline().document.nodes()[0].clone(); |
| 1755 | assert_eq!(editor.definitions[&node.style.unwrap()], heading); |
| 1756 | let spans = node.text().unwrap().text.spans().to_vec(); |
| 1757 | assert_eq!(spans.len(), 1); |
| 1758 | assert_eq!( |
| 1759 | spans[0].format, |
| 1760 | Format { |
| 1761 | language: Some(0x40c), |
| 1762 | ..heading.format.clone() |
| 1763 | } |
| 1764 | ); |
| 1765 | assert_eq!(editor.format_state().unwrap().style.as_deref(), Some("h2")); |
| 1766 | let length = node.text().unwrap().text.text().len() as u32; |
| 1767 | editor.select([at(0, length); 2].into()).unwrap(); |
| 1768 | editor.enter(&mut engine, false).unwrap(); |
| 1769 | let next = editor.active_outline().document.nodes()[1].style; |
| 1770 | assert_eq!(editor.definitions[&next.unwrap()], editor.styles["p"]); |
| 1771 | // Enter after Normal keeps Normal. |
| 1772 | editor.insert(&mut engine, "body").unwrap(); |
| 1773 | editor.enter(&mut engine, false).unwrap(); |
| 1774 | assert_eq!(editor.active_outline().document.nodes()[2].style, next); |
| 1775 | editor.undo(&mut engine).unwrap(); |
| 1776 | editor.undo(&mut engine).unwrap(); |
| 1777 | editor.undo(&mut engine).unwrap(); |
| 1778 | editor.undo(&mut engine).unwrap(); |
| 1779 | assert_eq!(editor.active_outline().document.nodes()[0].style, None); |
| 1780 | } |
| 1781 | |
| 1782 | #[test] |
| 1783 | fn clearing_and_removing_attributes_return_to_the_paragraph_style() { |
| 1784 | let mut engine = TextEngine::default(); |
| 1785 | let style = new_id().unwrap(); |
| 1786 | let heading = Format { |
| 1787 | bold: Some(true), |
| 1788 | color: Some(0x0012_3456), |
| 1789 | font: Some("Georgia".into()), |
| 1790 | font_size: Some(18.0), |
| 1791 | ..Format::default() |
| 1792 | }; |
| 1793 | let mut nodes = TextDocument::new(vec![Paragraph::new("Heading".into(), heading.clone())]) |
| 1794 | .unwrap() |
| 1795 | .nodes() |
| 1796 | .to_vec(); |
| 1797 | nodes[0].style = Some(style); |
| 1798 | let outline = Outline { |
| 1799 | id: new_id().unwrap(), |
| 1800 | title: false, |
| 1801 | min_width: None, |
| 1802 | layout: onestore::document::Layout { |
| 1803 | max_width: Some(300.0), |
| 1804 | ..Default::default() |
| 1805 | }, |
| 1806 | indents: vec![18.0, 0.0, 27.0, 27.0], |
| 1807 | paragraphs: nodes, |
| 1808 | unsupported: Vec::new(), |
| 1809 | }; |
| 1810 | let definitions = BTreeMap::from([( |
| 1811 | style, |
| 1812 | Definition { |
| 1813 | kind: Kind::Style { |
| 1814 | name: Some("h1".into()), |
| 1815 | next: None, |
| 1816 | }, |
| 1817 | format: heading.clone(), |
| 1818 | }, |
| 1819 | )]); |
| 1820 | let mut editor = |
| 1821 | CanvasEditor::from_outlines(&mut engine, vec![outline], definitions).unwrap(); |
| 1822 | editor.select_all().unwrap(); |
| 1823 | let state = editor.format_state().unwrap(); |
| 1824 | assert_eq!(state.toggles, [Toggle::Bold]); |
| 1825 | assert_eq!( |
| 1826 | (state.font.as_deref(), state.font_size, state.alignment), |
| 1827 | (Some("Georgia"), Some(18.0), Some(Alignment::Left)) |
| 1828 | ); |
| 1829 | for command in [ |
| 1830 | Formatting::Toggle(Toggle::Bold), |
| 1831 | Formatting::Color(None), |
| 1832 | Formatting::Highlight(Some(0x0000_ffff)), |
| 1833 | Formatting::Font("Arial".into()), |
| 1834 | Formatting::Align(Alignment::Center), |
| 1835 | ] { |
| 1836 | editor.format(&mut engine, command).unwrap(); |
| 1837 | } |
| 1838 | editor.select([at(0, 0), at(0, 4)].into()).unwrap(); |
| 1839 | editor |
| 1840 | .format(&mut engine, Formatting::FontSize(9.0)) |
| 1841 | .unwrap(); |
| 1842 | editor.select_all().unwrap(); |
| 1843 | assert_eq!(editor.format_state().unwrap().font_size, None); |
| 1844 | let format = editor |
| 1845 | .active_outline() |
| 1846 | .document |
| 1847 | .paragraph(0) |
| 1848 | .unwrap() |
| 1849 | .spans()[1] |
| 1850 | .format |
| 1851 | .clone(); |
| 1852 | assert_eq!( |
| 1853 | format, |
| 1854 | Format { |
| 1855 | bold: Some(false), |
| 1856 | color: Some(AUTOMATIC), |
| 1857 | highlight: Some(0x0000_ffff), |
| 1858 | font: Some("Arial".into()), |
| 1859 | alignment: Some(1), |
| 1860 | ..heading.clone() |
| 1861 | } |
| 1862 | ); |
| 1863 | editor.format(&mut engine, Formatting::Clear).unwrap(); |
| 1864 | assert_eq!( |
| 1865 | runs(&editor, Clone::clone), |
| 1866 | [vec![( |
| 1867 | "Heading".into(), |
| 1868 | Format { |
| 1869 | alignment: Some(1), |
| 1870 | ..heading |
| 1871 | } |
| 1872 | )]] |
| 1873 | ); |
| 1874 | editor |
| 1875 | .format(&mut engine, Formatting::Highlight(None)) |
| 1876 | .unwrap(); |
| 1877 | for _ in 0..7 { |
| 1878 | assert!(editor.undo(&mut engine).unwrap()); |
| 1879 | } |
| 1880 | assert!(!editor.undo(&mut engine).unwrap()); |
| 1881 | } |
| 1882 | |
| 1883 | /// Each paragraph's number as laid out, checked against laying the outline out afresh. |
| 1884 | fn numbers(engine: &mut TextEngine, editor: &CanvasEditor) -> Vec<Option<u32>> { |
| 1885 | let outline = editor.active_outline(); |
| 1886 | let fresh = OutlineLayout::flow( |
| 1887 | outline.document.nodes().iter(), |
| 1888 | &outline.indents, |
| 1889 | outline.wrap_width(), |
| 1890 | true, |
| 1891 | 0, |
| 1892 | None, |
| 1893 | &mut |node, previous, width, indents| { |
| 1894 | ParagraphLayout::shape(engine, node, previous, width, indents, &editor.definitions) |
| 1895 | }, |
| 1896 | ) |
| 1897 | .unwrap(); |
| 1898 | let laid = |layout: &OutlineLayout| { |
| 1899 | layout |
| 1900 | .paragraphs |
| 1901 | .iter() |
| 1902 | .map(|paragraph| { |
| 1903 | let markers = paragraph.markers.iter().map(|(marker, origin)| { |
| 1904 | (*origin, marker.lines().next().unwrap().0.metrics().advance) |
| 1905 | }); |
| 1906 | (paragraph.number.clone(), markers.collect::<Vec<_>>()) |
| 1907 | }) |
| 1908 | .collect::<Vec<_>>() |
| 1909 | }; |
| 1910 | assert_eq!(laid(&outline.shaped), laid(&fresh)); |
| 1911 | outline |
| 1912 | .shaped |
| 1913 | .paragraphs |
| 1914 | .iter() |
| 1915 | .map(|paragraph| paragraph.number.as_ref().map(|(count, _)| count.number)) |
| 1916 | .collect() |
| 1917 | } |
| 1918 | |
| 1919 | #[test] |
| 1920 | fn lists_toggle_switch_kind_and_renumber_only_what_changes() { |
| 1921 | let mut engine = TextEngine::default(); |
| 1922 | let mut editor = plain(&mut engine, &["one", "two", "three", "four"]); |
| 1923 | let original = editor.active_outline().document.clone(); |
| 1924 | let all = Selection::from([at(0, 0), at(3, 4)]); |
| 1925 | editor.select(all).unwrap(); |
| 1926 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 1927 | let state = editor.format_state().unwrap(); |
| 1928 | assert!(state.bullets && !state.numbering); |
| 1929 | assert_eq!(numbers(&mut engine, &editor), [None; 4]); |
| 1930 | let bullet = editor.active_outline().document.nodes()[0].lists[0]; |
| 1931 | assert_eq!( |
| 1932 | editor.definitions[&bullet], |
| 1933 | list_definition(ListStyle::BULLET, &calibri()) |
| 1934 | ); |
| 1935 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 1936 | let state = editor.format_state().unwrap(); |
| 1937 | assert!(!state.bullets && state.numbering); |
| 1938 | assert_eq!( |
| 1939 | numbers(&mut engine, &editor), |
| 1940 | [Some(1), Some(2), Some(3), Some(4)] |
| 1941 | ); |
| 1942 | let untouched = editor.active_outline().shaped.paragraphs[0] |
| 1943 | .text |
| 1944 | .shaped |
| 1945 | .styles() |
| 1946 | .as_ptr(); |
| 1947 | editor.select([at(1, 1); 2].into()).unwrap(); |
| 1948 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 1949 | // An unnumbered sibling leaves the count alone (`evidence/structural-edits/xml/c8-bs-1.xml`). |
| 1950 | assert_eq!( |
| 1951 | numbers(&mut engine, &editor), |
| 1952 | [Some(1), None, Some(2), Some(3)] |
| 1953 | ); |
| 1954 | assert_eq!( |
| 1955 | editor.active_outline().shaped.paragraphs[0] |
| 1956 | .text |
| 1957 | .shaped |
| 1958 | .styles() |
| 1959 | .as_ptr(), |
| 1960 | untouched |
| 1961 | ); |
| 1962 | editor.undo(&mut engine).unwrap(); |
| 1963 | editor.select([at(2, 0); 2].into()).unwrap(); |
| 1964 | editor.format(&mut engine, Formatting::Indent).unwrap(); |
| 1965 | assert_eq!( |
| 1966 | numbers(&mut engine, &editor), |
| 1967 | [Some(1), Some(2), Some(1), Some(3)] |
| 1968 | ); |
| 1969 | assert_eq!(editor.selection(), Selection::from([at(2, 0); 2])); |
| 1970 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 1971 | assert_eq!( |
| 1972 | numbers(&mut engine, &editor), |
| 1973 | [Some(1), Some(2), None, Some(3)] |
| 1974 | ); |
| 1975 | assert!(editor.format_state().unwrap().bullets); |
| 1976 | editor.format(&mut engine, Formatting::Outdent).unwrap(); |
| 1977 | assert_eq!( |
| 1978 | numbers(&mut engine, &editor), |
| 1979 | [Some(1), Some(2), None, Some(3)] |
| 1980 | ); |
| 1981 | editor.select(all).unwrap(); |
| 1982 | let state = editor.format_state().unwrap(); |
| 1983 | assert!(!state.bullets && !state.numbering); |
| 1984 | for _ in 0..6 { |
| 1985 | editor.undo(&mut engine).unwrap(); |
| 1986 | } |
| 1987 | assert_eq!(editor.active_outline().document, original); |
| 1988 | assert_eq!(numbers(&mut engine, &editor), [None; 4]); |
| 1989 | } |
| 1990 | |
| 1991 | #[test] |
| 1992 | fn library_styles_replace_any_list_and_none_removes_them() { |
| 1993 | let mut engine = TextEngine::default(); |
| 1994 | let mut editor = plain(&mut engine, &["one", "two", "three"]); |
| 1995 | let original = editor.active_outline().document.clone(); |
| 1996 | let all = Selection::from([at(0, 0), at(2, 5)]); |
| 1997 | editor.select(all).unwrap(); |
| 1998 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 1999 | assert_eq!(editor.format_state().unwrap().list, Some(ListStyle::BULLET)); |
| 2000 | let pick = |style| Formatting::List(Some(style)); |
| 2001 | editor |
| 2002 | .format(&mut engine, pick(ListStyle::Bullet(0))) |
| 2003 | .unwrap(); |
| 2004 | let state = editor.format_state().unwrap(); |
| 2005 | assert!(state.bullets && !state.numbering); |
| 2006 | assert_eq!(state.list, Some(ListStyle::Bullet(0))); |
| 2007 | let first = editor.active_outline().document.nodes()[0].lists[0]; |
| 2008 | assert_eq!( |
| 2009 | editor.definitions[&first], |
| 2010 | list_definition(ListStyle::Bullet(0), &calibri()) |
| 2011 | ); |
| 2012 | // Picking the style again leaves the lists as they are. |
| 2013 | let stored = editor.active_outline().document.clone(); |
| 2014 | editor |
| 2015 | .format(&mut engine, pick(ListStyle::Bullet(0))) |
| 2016 | .unwrap(); |
| 2017 | assert_eq!(editor.active_outline().document, stored); |
| 2018 | // A number format replaces the bullets; "First." lays out as any number does. |
| 2019 | editor |
| 2020 | .format(&mut engine, pick(ListStyle::Number(17))) |
| 2021 | .unwrap(); |
| 2022 | let state = editor.format_state().unwrap(); |
| 2023 | assert!(state.numbering && !state.bullets); |
| 2024 | assert_eq!(state.list, Some(ListStyle::Number(17))); |
| 2025 | assert_eq!(numbers(&mut engine, &editor), [Some(1), Some(2), Some(3)]); |
| 2026 | // Paragraphs of different styles share none. |
| 2027 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2028 | editor |
| 2029 | .format(&mut engine, pick(ListStyle::Number(0))) |
| 2030 | .unwrap(); |
| 2031 | editor.select(all).unwrap(); |
| 2032 | let state = editor.format_state().unwrap(); |
| 2033 | assert!(state.numbering && state.list.is_none()); |
| 2034 | editor.format(&mut engine, Formatting::List(None)).unwrap(); |
| 2035 | let state = editor.format_state().unwrap(); |
| 2036 | assert!(!state.bullets && !state.numbering && state.list.is_none()); |
| 2037 | assert_eq!(numbers(&mut engine, &editor), [None; 3]); |
| 2038 | for _ in 0..6 { |
| 2039 | editor.undo(&mut engine).unwrap(); |
| 2040 | } |
| 2041 | assert_eq!(editor.active_outline().document, original); |
| 2042 | } |
| 2043 | |
| 2044 | #[test] |
| 2045 | fn restyling_a_list_after_a_plain_sibling_leaves_it_where_it_is() { |
| 2046 | let mut engine = TextEngine::default(); |
| 2047 | let mut editor = plain(&mut engine, &["head", "item"]); |
| 2048 | let levels = |editor: &CanvasEditor| { |
| 2049 | editor |
| 2050 | .active_outline() |
| 2051 | .document |
| 2052 | .nodes() |
| 2053 | .iter() |
| 2054 | .map(|node| node.level) |
| 2055 | .collect::<Vec<_>>() |
| 2056 | }; |
| 2057 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2058 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 2059 | assert_eq!(levels(&editor), [1, 2]); |
| 2060 | editor.format(&mut engine, Formatting::Outdent).unwrap(); |
| 2061 | assert_eq!(levels(&editor), [1, 1]); |
| 2062 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 2063 | editor |
| 2064 | .format(&mut engine, Formatting::List(Some(ListStyle::Bullet(0)))) |
| 2065 | .unwrap(); |
| 2066 | assert_eq!(levels(&editor), [1, 1]); |
| 2067 | assert_eq!( |
| 2068 | editor.format_state().unwrap().list, |
| 2069 | Some(ListStyle::Bullet(0)) |
| 2070 | ); |
| 2071 | } |
| 2072 | |
| 2073 | #[test] |
| 2074 | fn tab_steps_through_onenotes_nested_bullets_and_numbers() { |
| 2075 | let chain = |start: ListStyle, steps: usize| { |
| 2076 | let mut definition = list_definition(start, &calibri()); |
| 2077 | let mut styles = vec![ListStyle::of(&definition.kind).unwrap()]; |
| 2078 | for _ in 0..steps { |
| 2079 | definition = nested_list(&definition, true).unwrap(); |
| 2080 | styles.push(ListStyle::of(&definition.kind).unwrap()); |
| 2081 | } |
| 2082 | (styles, definition) |
| 2083 | }; |
| 2084 | let (bullets, deepest) = chain(ListStyle::BULLET, 9); |
| 2085 | assert_eq!( |
| 2086 | bullets, |
| 2087 | [2, 3, 13, 14, 9, 7, 15, 26, 8, 2].map(ListStyle::Bullet) |
| 2088 | ); |
| 2089 | // Wingdings § is ListMSAAIndex 9 here, as the gallery stores it. |
| 2090 | assert_eq!(BULLET_LIBRARY[13], ("Wingdings", "\u{a7}", 9)); |
| 2091 | assert_eq!( |
| 2092 | deepest.format, |
| 2093 | list_definition(ListStyle::BULLET, &calibri()).format |
| 2094 | ); |
| 2095 | let (numbers, _) = chain(ListStyle::NUMBER, 8); |
| 2096 | assert_eq!( |
| 2097 | numbers, |
| 2098 | [0, 2, 4, 6, 7, 9, 16, 17, 0].map(ListStyle::Number) |
| 2099 | ); |
| 2100 | let second = nested_list(&list_definition(ListStyle::NUMBER, &calibri()), true).unwrap(); |
| 2101 | assert_eq!( |
| 2102 | ListStyle::of(&nested_list(&second, false).unwrap().kind), |
| 2103 | Some(ListStyle::NUMBER) |
| 2104 | ); |
| 2105 | assert!(nested_list(&list_definition(ListStyle::BULLET, &calibri()), false).is_none()); |
| 2106 | // A style off both paths keeps its list. |
| 2107 | assert!(nested_list(&list_definition(ListStyle::Bullet(33), &calibri()), true).is_none()); |
| 2108 | } |
| 2109 | |
| 2110 | #[test] |
| 2111 | fn every_library_style_reads_back_as_itself() { |
| 2112 | let styles = (0..BULLET_LIBRARY.len()) |
| 2113 | .map(ListStyle::Bullet) |
| 2114 | .chain((0..NUMBER_LIBRARY.len()).map(ListStyle::Number)); |
| 2115 | for style in styles { |
| 2116 | let definition = list_definition(style, &calibri()); |
| 2117 | assert_eq!(ListStyle::of(&definition.kind), Some(style)); |
| 2118 | } |
| 2119 | } |
| 2120 | |
| 2121 | #[test] |
| 2122 | fn numbering_follows_stored_sequences_restarts_and_nesting() { |
| 2123 | let mut engine = TextEngine::default(); |
| 2124 | let mut editor = plain(&mut engine, &["a", "b", "c", "d", "e"]); |
| 2125 | let mut nodes = editor.active_outline().document.nodes().to_vec(); |
| 2126 | for (node, (sequence, restart, level)) in nodes.iter_mut().zip([ |
| 2127 | ('\u{1}', None, 1), |
| 2128 | ('\u{1}', None, 1), |
| 2129 | ('\u{4}', None, 2), |
| 2130 | ('\u{1}', Some(9), 1), |
| 2131 | ('\u{1}', None, 1), |
| 2132 | ]) { |
| 2133 | let id = new_id().unwrap(); |
| 2134 | let mut definition = list_definition(ListStyle::NUMBER, &calibri()); |
| 2135 | let Kind::List { |
| 2136 | format, |
| 2137 | restart: value, |
| 2138 | .. |
| 2139 | } = &mut definition.kind |
| 2140 | else { |
| 2141 | unreachable!() |
| 2142 | }; |
| 2143 | *format = Some(format!("(\u{fffd}{sequence})")); |
| 2144 | *value = restart; |
| 2145 | editor.definitions.insert(id, definition); |
| 2146 | node.lists = vec![id]; |
| 2147 | node.level = level; |
| 2148 | } |
| 2149 | let outline = Outline { |
| 2150 | paragraphs: nodes, |
| 2151 | ..editor.active_outline().snapshot() |
| 2152 | }; |
| 2153 | let editor = |
| 2154 | CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap(); |
| 2155 | assert_eq!( |
| 2156 | numbers(&mut engine, &editor), |
| 2157 | [Some(1), Some(2), Some(1), Some(9), Some(10)] |
| 2158 | ); |
| 2159 | } |
| 2160 | |
| 2161 | /// OneNote 2010's numbers for siblings and children in the formats and levels given, as |
| 2162 | /// its COM interface reports them (`evidence/toolbar-17/restart.txt`); `None` is a plain |
| 2163 | /// paragraph, `Some("•")` a bullet. |
| 2164 | #[test] |
| 2165 | fn numbers_restart_where_the_format_changes() { |
| 2166 | let numbered = |paragraphs: &[(Option<&str>, u32)]| { |
| 2167 | let mut engine = TextEngine::default(); |
| 2168 | let texts = vec!["x"; paragraphs.len()]; |
| 2169 | let mut editor = plain(&mut engine, &texts); |
| 2170 | let mut nodes = editor.active_outline().document.nodes().to_vec(); |
| 2171 | for (node, (list, level)) in nodes.iter_mut().zip(paragraphs) { |
| 2172 | node.level = *level; |
| 2173 | let style = match list { |
| 2174 | None => continue, |
| 2175 | Some("•") => ListStyle::BULLET, |
| 2176 | Some(_) => ListStyle::NUMBER, |
| 2177 | }; |
| 2178 | let mut definition = list_definition(style, &calibri()); |
| 2179 | if let (Some(value), Kind::List { format, .. }) = (list, &mut definition.kind) |
| 2180 | && style == ListStyle::NUMBER |
| 2181 | { |
| 2182 | *format = Some((*value).into()); |
| 2183 | } |
| 2184 | let id = new_id().unwrap(); |
| 2185 | editor.definitions.insert(id, definition); |
| 2186 | node.lists = vec![id]; |
| 2187 | } |
| 2188 | let outline = Outline { |
| 2189 | paragraphs: nodes, |
| 2190 | ..editor.active_outline().snapshot() |
| 2191 | }; |
| 2192 | let editor = |
| 2193 | CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions) |
| 2194 | .unwrap(); |
| 2195 | numbers(&mut engine, &editor) |
| 2196 | }; |
| 2197 | let [dot, paren, letter] = ["\u{fffd}\u{0}.", "\u{fffd}\u{0})", "\u{fffd}\u{4}."]; |
| 2198 | // Another format starts again, and so does the first format after it. |
| 2199 | assert_eq!( |
| 2200 | numbered(&[ |
| 2201 | (Some(dot), 1), |
| 2202 | (Some(dot), 1), |
| 2203 | (Some(paren), 1), |
| 2204 | (Some(dot), 1), |
| 2205 | (Some(paren), 1), |
| 2206 | ]), |
| 2207 | [Some(1), Some(2), Some(1), Some(1), Some(1)] |
| 2208 | ); |
| 2209 | // Plain and bulleted siblings leave the count alone. |
| 2210 | assert_eq!( |
| 2211 | numbered(&[(Some(dot), 1), (None, 1), (Some("•"), 1), (Some(dot), 1)]), |
| 2212 | [Some(1), None, None, Some(2)] |
| 2213 | ); |
| 2214 | // So does another sequence; children count apart from their parents, afresh under each. |
| 2215 | assert_eq!( |
| 2216 | numbered(&[(Some(dot), 1), (Some(letter), 1), (Some(dot), 1)]), |
| 2217 | [Some(1), Some(1), Some(1)] |
| 2218 | ); |
| 2219 | assert_eq!( |
| 2220 | numbered(&[ |
| 2221 | (Some(dot), 1), |
| 2222 | (Some(dot), 2), |
| 2223 | (Some(dot), 2), |
| 2224 | (Some(dot), 1), |
| 2225 | (Some(dot), 2), |
| 2226 | (None, 1), |
| 2227 | (Some(dot), 1), |
| 2228 | ]), |
| 2229 | [Some(1), Some(1), Some(2), Some(2), Some(1), None, Some(3)] |
| 2230 | ); |
| 2231 | } |
| 2232 | |
| 2233 | fn text_tags(editor: &CanvasEditor, paragraph: usize) -> Vec<(String, u16, bool)> { |
| 2234 | let (_, _, node) = editor.active_outline().document.leaf(paragraph).unwrap(); |
| 2235 | tags(node) |
| 2236 | .map(|tag| { |
| 2237 | let Some(Kind::TagDefinition { label, .. }) = editor.tag_kind(tag) else { |
| 2238 | panic!("{tag:?}") |
| 2239 | }; |
| 2240 | let settled = tag.completed |
| 2241 | == if tag.status & 1 == 0 { |
| 2242 | Some(0) |
| 2243 | } else { |
| 2244 | tag.created |
| 2245 | }; |
| 2246 | ( |
| 2247 | label.clone().unwrap(), |
| 2248 | tag.status, |
| 2249 | settled || tag.status & 1 == 1, |
| 2250 | ) |
| 2251 | }) |
| 2252 | .collect() |
| 2253 | } |
| 2254 | |
| 2255 | /// OneNote 2010 checks a clicked To Do box (status 1, completion time now) and clears it |
| 2256 | /// (status 0, completion time kept as zero) without moving the caret. |
| 2257 | #[test] |
| 2258 | fn clicking_a_check_box_checks_and_clears_it_as_one_step() { |
| 2259 | let mut engine = TextEngine::default(); |
| 2260 | let mut editor = plain(&mut engine, &["task", "other"]); |
| 2261 | editor.select([at(0, 0), at(1, 5)].into()).unwrap(); |
| 2262 | editor |
| 2263 | .format( |
| 2264 | &mut engine, |
| 2265 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2266 | ) |
| 2267 | .unwrap(); |
| 2268 | editor.select([at(1, 2); 2].into()).unwrap(); |
| 2269 | let outline = editor.active_outline().id; |
| 2270 | let task = editor.active_outline().document.nodes()[0].id; |
| 2271 | let selection = editor.selection(); |
| 2272 | editor.click_check(&mut engine, outline, task).unwrap(); |
| 2273 | assert_eq!(editor.selection(), selection); |
| 2274 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 1, true)]); |
| 2275 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]); |
| 2276 | let tag = |editor: &CanvasEditor| { |
| 2277 | editor.active_outline().document.nodes()[0] |
| 2278 | .text() |
| 2279 | .unwrap() |
| 2280 | .tags[0] |
| 2281 | .clone() |
| 2282 | }; |
| 2283 | assert!(tag(&editor).completed.is_some_and(|time| time > 0)); |
| 2284 | editor.click_check(&mut engine, outline, task).unwrap(); |
| 2285 | assert_eq!((tag(&editor).status, tag(&editor).completed), (0, Some(0))); |
| 2286 | editor.undo(&mut engine).unwrap(); |
| 2287 | assert_eq!(tag(&editor).status, 1); |
| 2288 | } |
| 2289 | |
| 2290 | #[test] |
| 2291 | fn tags_toggle_check_and_replace_tags_of_their_action_type() { |
| 2292 | let mut engine = TextEngine::default(); |
| 2293 | let mut editor = plain(&mut engine, &["task", "other", "music"]); |
| 2294 | let music = new_id().unwrap(); |
| 2295 | editor.definitions.insert( |
| 2296 | music, |
| 2297 | Definition { |
| 2298 | kind: Kind::TagDefinition { |
| 2299 | label: Some("Music".into()), |
| 2300 | action_type: Some(2), |
| 2301 | shape: Some(121), |
| 2302 | color: None, |
| 2303 | highlight: None, |
| 2304 | }, |
| 2305 | format: Format::default(), |
| 2306 | }, |
| 2307 | ); |
| 2308 | let mut nodes = editor.active_outline().document.nodes().to_vec(); |
| 2309 | nodes[2].tags.push(Tag { |
| 2310 | definition: Some(music), |
| 2311 | action_type: None, |
| 2312 | shape: None, |
| 2313 | property_status: None, |
| 2314 | status: 1, |
| 2315 | created: Some(1), |
| 2316 | completed: Some(1), |
| 2317 | start: None, |
| 2318 | due: None, |
| 2319 | task_id: None, |
| 2320 | extra_set: 0, |
| 2321 | }); |
| 2322 | let outline = Outline { |
| 2323 | paragraphs: nodes, |
| 2324 | ..editor.active_outline().snapshot() |
| 2325 | }; |
| 2326 | let mut editor = |
| 2327 | CanvasEditor::from_outlines(&mut engine, vec![outline], editor.definitions).unwrap(); |
| 2328 | let original = editor.active_outline().document.clone(); |
| 2329 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 2330 | editor |
| 2331 | .format( |
| 2332 | &mut engine, |
| 2333 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2334 | ) |
| 2335 | .unwrap(); |
| 2336 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]); |
| 2337 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]); |
| 2338 | assert_eq!( |
| 2339 | editor.format_state().unwrap().tags, |
| 2340 | [(NoteTag::defaults()[0].clone(), 0)] |
| 2341 | ); |
| 2342 | let icon = |
| 2343 | |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon; |
| 2344 | assert_eq!( |
| 2345 | icon(&editor), |
| 2346 | crate::outline::TagIcon::Symbol { |
| 2347 | shape: 3, |
| 2348 | checked: false |
| 2349 | } |
| 2350 | ); |
| 2351 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 2352 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 2353 | assert_eq!( |
| 2354 | icon(&editor), |
| 2355 | crate::outline::TagIcon::Symbol { |
| 2356 | shape: 3, |
| 2357 | checked: true |
| 2358 | } |
| 2359 | ); |
| 2360 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2361 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 2362 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 2363 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 2364 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 1, true)]); |
| 2365 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 2366 | |
| 2367 | editor.select([at(1, 0), at(2, 5)].into()).unwrap(); |
| 2368 | editor |
| 2369 | .format( |
| 2370 | &mut engine, |
| 2371 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), |
| 2372 | ) |
| 2373 | .unwrap(); |
| 2374 | assert_eq!( |
| 2375 | text_tags(&editor, 1), |
| 2376 | [("Question".into(), 1, true), ("To Do".into(), 1, true)] |
| 2377 | ); |
| 2378 | assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]); |
| 2379 | assert_eq!( |
| 2380 | editor.format_state().unwrap().tags, |
| 2381 | [(NoteTag::defaults()[2].clone(), 2)] |
| 2382 | ); |
| 2383 | let questions = editor |
| 2384 | .definitions |
| 2385 | .values() |
| 2386 | .filter(|definition| **definition == NoteTag::defaults()[2].definition(2)) |
| 2387 | .count(); |
| 2388 | assert_eq!(questions, 1); |
| 2389 | editor |
| 2390 | .format( |
| 2391 | &mut engine, |
| 2392 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), |
| 2393 | ) |
| 2394 | .unwrap(); |
| 2395 | assert_eq!(text_tags(&editor, 2), []); |
| 2396 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 2397 | editor |
| 2398 | .format( |
| 2399 | &mut engine, |
| 2400 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2401 | ) |
| 2402 | .unwrap(); |
| 2403 | assert_eq!(text_tags(&editor, 0), []); |
| 2404 | for _ in 0..7 { |
| 2405 | editor.undo(&mut engine).unwrap(); |
| 2406 | } |
| 2407 | assert!(!editor.undo(&mut engine).unwrap()); |
| 2408 | assert_eq!(editor.active_outline().document, original); |
| 2409 | } |
| 2410 | |
| 2411 | #[test] |
| 2412 | fn lists_become_to_do_lists_and_back_as_one_undo_step_each() { |
| 2413 | let mut engine = TextEngine::default(); |
| 2414 | let mut editor = plain(&mut engine, &["one", "two", "three"]); |
| 2415 | editor.select([at(0, 0), at(1, 3)].into()).unwrap(); |
| 2416 | editor.format(&mut engine, Formatting::Bullets).unwrap(); |
| 2417 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2418 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 2419 | let listed = editor.active_outline().document.clone(); |
| 2420 | let all = Selection::from([at(0, 0), at(2, 5)]); |
| 2421 | editor.select(all).unwrap(); |
| 2422 | assert!(editor.format_state().unwrap().listed); |
| 2423 | let to_do = NoteTag::defaults()[0].clone(); |
| 2424 | editor |
| 2425 | .format(&mut engine, Formatting::ToDoList(to_do.clone(), 0)) |
| 2426 | .unwrap(); |
| 2427 | let lists = |editor: &CanvasEditor| { |
| 2428 | let document = &editor.active_outline().document; |
| 2429 | document |
| 2430 | .nodes() |
| 2431 | .iter() |
| 2432 | .map(|node| editor.list(node)) |
| 2433 | .collect::<Vec<_>>() |
| 2434 | }; |
| 2435 | assert_eq!(lists(&editor), [None, None, None]); |
| 2436 | let task = || vec![("To Do".to_owned(), 0, true)]; |
| 2437 | assert_eq!( |
| 2438 | (0..3).map(|at| text_tags(&editor, at)).collect::<Vec<_>>(), |
| 2439 | [task(), task(), vec![]] |
| 2440 | ); |
| 2441 | let state = editor.format_state().unwrap(); |
| 2442 | assert!(!state.listed && state.tags.is_empty()); |
| 2443 | editor.select([at(0, 0), at(1, 3)].into()).unwrap(); |
| 2444 | assert_eq!(editor.format_state().unwrap().tags, [(to_do.clone(), 0)]); |
| 2445 | editor |
| 2446 | .format(&mut engine, Formatting::BulletedList(to_do, 0)) |
| 2447 | .unwrap(); |
| 2448 | assert_eq!( |
| 2449 | lists(&editor), |
| 2450 | [Some(Formatting::Bullets), Some(Formatting::Bullets), None] |
| 2451 | ); |
| 2452 | assert_eq!(text_tags(&editor, 0), []); |
| 2453 | assert_eq!(text_tags(&editor, 1), []); |
| 2454 | let state = editor.format_state().unwrap(); |
| 2455 | assert_eq!((state.list, state.tags), (Some(ListStyle::BULLET), vec![])); |
| 2456 | editor.undo(&mut engine).unwrap(); |
| 2457 | assert_eq!(text_tags(&editor, 1), task()); |
| 2458 | editor.undo(&mut engine).unwrap(); |
| 2459 | assert_eq!(editor.active_outline().document, listed); |
| 2460 | } |
| 2461 | |
| 2462 | #[test] |
| 2463 | fn the_default_tags_are_onenotes_stored_definitions_and_all_draw() { |
| 2464 | use onestore::{RevisionIndex, Store, document::Document}; |
| 2465 | let bytes = include_bytes!("../../../../corpus/structural-probe/tag-gallery.one"); |
| 2466 | let store = Store::parse(bytes).unwrap(); |
| 2467 | let index = RevisionIndex::parse(&store).unwrap(); |
| 2468 | let document = Document::parse(&index).unwrap(); |
| 2469 | let (space, _) = document.pages().unwrap()[0]; |
| 2470 | let stored = Page::from_space(&document, space).unwrap().definitions; |
| 2471 | let stored = stored |
| 2472 | .values() |
| 2473 | .filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. })) |
| 2474 | .collect::<Vec<_>>(); |
| 2475 | let defaults = NoteTag::defaults(); |
| 2476 | assert_eq!(stored.len(), defaults.len()); |
| 2477 | for (place, tag) in defaults.iter().enumerate() { |
| 2478 | assert!(stored.contains(&&tag.definition(place as u16)), "{tag:?}"); |
| 2479 | } |
| 2480 | let mut engine = TextEngine::default(); |
| 2481 | let mut editor = plain(&mut engine, &["tagged"]); |
| 2482 | for (place, tag) in defaults.iter().enumerate() { |
| 2483 | editor |
| 2484 | .format(&mut engine, Formatting::Tag(tag.clone(), place as u16)) |
| 2485 | .unwrap(); |
| 2486 | } |
| 2487 | assert_eq!(editor.format_state().unwrap().tags.len(), defaults.len()); |
| 2488 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; |
| 2489 | let shapes = paragraph |
| 2490 | .tags |
| 2491 | .iter() |
| 2492 | .map(|tag| match tag.icon { |
| 2493 | crate::outline::TagIcon::Symbol { shape, .. } => shape, |
| 2494 | crate::outline::TagIcon::Task { .. } => unreachable!(), |
| 2495 | }) |
| 2496 | .collect::<Vec<_>>(); |
| 2497 | assert_eq!(shapes.len(), defaults.len() - 2); |
| 2498 | assert_eq!(shapes[..7], [3, 13, 15, 136, 118, 23, 18]); |
| 2499 | // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back, |
| 2500 | // the priorities and Client request. |
| 2501 | let checkable = defaults |
| 2502 | .iter() |
| 2503 | .filter(|tag| crate::outline::checkable(tag.shape)) |
| 2504 | .count(); |
| 2505 | assert_eq!(checkable, 9); |
| 2506 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. |
| 2507 | assert!( |
| 2508 | paragraph |
| 2509 | .text |
| 2510 | .backgrounds() |
| 2511 | .all(|(_, color)| color == 0x0000_ff00) |
| 2512 | ); |
| 2513 | assert!(paragraph.text.backgrounds().next().is_some()); |
| 2514 | } |
| 2515 | |
| 2516 | /// A customized tag stores its name, symbol and colours with its place in the list as |
| 2517 | /// its action type; applied from another place it takes a definition of its own, as |
| 2518 | /// OneNote 2010's does (`corpus/custom-tags`). |
| 2519 | #[test] |
| 2520 | fn custom_tags_store_their_look_and_place() { |
| 2521 | let mut engine = TextEngine::default(); |
| 2522 | let mut editor = plain(&mut engine, &["one", "two"]); |
| 2523 | let tag = NoteTag { |
| 2524 | label: "Snow check".into(), |
| 2525 | shape: 61, |
| 2526 | color: Some(0x0000_0080), |
| 2527 | highlight: Some(0x00ff_cc00), |
| 2528 | art: None, |
| 2529 | }; |
| 2530 | editor |
| 2531 | .format(&mut engine, Formatting::Tag(tag.clone(), 0)) |
| 2532 | .unwrap(); |
| 2533 | assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 0)]); |
| 2534 | assert_eq!(text_tags(&editor, 0), [("Snow check".into(), 1, true)]); |
| 2535 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2536 | editor |
| 2537 | .format(&mut engine, Formatting::Tag(tag.clone(), 1)) |
| 2538 | .unwrap(); |
| 2539 | assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 1)]); |
| 2540 | let mut stored: Vec<_> = editor |
| 2541 | .definitions |
| 2542 | .values() |
| 2543 | .filter_map(|definition| NoteTag::of(&definition.kind)) |
| 2544 | .collect(); |
| 2545 | stored.sort_by_key(|(_, action_type)| *action_type); |
| 2546 | assert_eq!(stored, [(tag.clone(), 0), (tag, 1)]); |
| 2547 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; |
| 2548 | assert!( |
| 2549 | paragraph |
| 2550 | .text |
| 2551 | .backgrounds() |
| 2552 | .all(|(_, color)| color == 0x00ff_cc00) |
| 2553 | ); |
| 2554 | assert!(paragraph.text.backgrounds().next().is_some()); |
| 2555 | } |
| 2556 | |
| 2557 | #[test] |
| 2558 | fn commands_reach_every_cell_a_selection_crosses() { |
| 2559 | let mut engine = TextEngine::default(); |
| 2560 | let mut editor = plain(&mut engine, &["abcdef"]); |
| 2561 | editor.select([at(0, 3); 2].into()).unwrap(); |
| 2562 | editor.tab(&mut engine, false).unwrap(); |
| 2563 | let table = editor.active_outline().document.clone(); |
| 2564 | assert_eq!(table.nodes().len(), 1); |
| 2565 | let selection = Selection::from([at(0, 1), at(1, 2)]); |
| 2566 | editor.select(selection).unwrap(); |
| 2567 | editor |
| 2568 | .format(&mut engine, Formatting::Toggle(Toggle::Italic)) |
| 2569 | .unwrap(); |
| 2570 | assert_eq!( |
| 2571 | runs(&editor, |format| format.italic), |
| 2572 | [ |
| 2573 | vec![run("a", None), run("bc", Some(true))], |
| 2574 | vec![run("de", Some(true)), run("f", None)] |
| 2575 | ] |
| 2576 | ); |
| 2577 | assert!( |
| 2578 | editor |
| 2579 | .format_state() |
| 2580 | .unwrap() |
| 2581 | .toggles |
| 2582 | .contains(&Toggle::Italic) |
| 2583 | ); |
| 2584 | editor |
| 2585 | .format(&mut engine, Formatting::Align(Alignment::Right)) |
| 2586 | .unwrap(); |
| 2587 | assert_eq!( |
| 2588 | editor.format_state().unwrap().alignment, |
| 2589 | Some(Alignment::Right) |
| 2590 | ); |
| 2591 | editor.select([at(1, 0); 2].into()).unwrap(); |
| 2592 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 2593 | assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]); |
| 2594 | editor |
| 2595 | .format( |
| 2596 | &mut engine, |
| 2597 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2598 | ) |
| 2599 | .unwrap(); |
| 2600 | assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1); |
| 2601 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); |
| 2602 | for _ in 0..4 { |
| 2603 | editor.undo(&mut engine).unwrap(); |
| 2604 | } |
| 2605 | assert_eq!(editor.active_outline().document, table); |
| 2606 | assert_eq!(editor.selection(), selection); |
| 2607 | } |
| 2608 | |
| 2609 | #[test] |
| 2610 | fn formatting_lists_and_tags_survive_the_page_writer() { |
| 2611 | use onestore::{RevisionIndex, Store, document::Document}; |
| 2612 | const SECTION: &[u8] = |
| 2613 | include_bytes!("../../../../corpus/paragraph-edit/before/notebook/synthetic.one"); |
| 2614 | let page = |bytes: &[u8]| { |
| 2615 | let store = Store::parse(bytes).unwrap(); |
| 2616 | let index = RevisionIndex::parse(&store).unwrap(); |
| 2617 | let document = Document::parse(&index).unwrap(); |
| 2618 | document |
| 2619 | .pages() |
| 2620 | .unwrap() |
| 2621 | .into_iter() |
| 2622 | .map(|(space, _)| (space, Page::from_space(&document, space).unwrap())) |
| 2623 | .find(|(_, page)| page.title == "Split middle") |
| 2624 | .unwrap() |
| 2625 | }; |
| 2626 | // Tags keep their place in the element's property arena only once read. |
| 2627 | let settled = |mut page: Page| { |
| 2628 | for object in &mut page.objects { |
| 2629 | if let PageObject::Outline(outline) = object { |
| 2630 | for text in outline.paragraphs.iter_mut().filter_map(|p| p.text_mut()) { |
| 2631 | for tag in &mut text.tags { |
| 2632 | tag.extra_set = 0; |
| 2633 | } |
| 2634 | } |
| 2635 | } |
| 2636 | } |
| 2637 | page |
| 2638 | }; |
| 2639 | let (space, source) = page(SECTION); |
| 2640 | let mut engine = TextEngine::default(); |
| 2641 | let mut editor = CanvasEditor::from_page(source, &mut engine).unwrap(); |
| 2642 | let body = editor |
| 2643 | .outlines() |
| 2644 | .iter() |
| 2645 | .find(|outline| !outline.title) |
| 2646 | .unwrap() |
| 2647 | .id; |
| 2648 | editor.focus_outline(body).unwrap(); |
| 2649 | let selection = Selection::from([at(0, 2), at(0, 6)]); |
| 2650 | editor.select(selection).unwrap(); |
| 2651 | for command in [ |
| 2652 | Formatting::Toggle(Toggle::Bold), |
| 2653 | Formatting::Toggle(Toggle::Superscript), |
| 2654 | Formatting::FontSize(14.0), |
| 2655 | Formatting::Color(Some(0x0000_00ff)), |
| 2656 | Formatting::Highlight(Some(0x0000_ffff)), |
| 2657 | Formatting::Align(Alignment::Center), |
| 2658 | Formatting::Numbering, |
| 2659 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2660 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), |
| 2661 | Formatting::Check, |
| 2662 | ] { |
| 2663 | editor.format(&mut engine, command).unwrap(); |
| 2664 | } |
| 2665 | let state = editor.format_state().unwrap(); |
| 2666 | let edited = settled(editor.page().unwrap()); |
| 2667 | let written = super::ops::saved(SECTION, space, &mut editor); |
| 2668 | let (_, reread) = page(&written); |
| 2669 | let reread = settled(reread); |
| 2670 | assert_eq!(reread.objects, edited.objects); |
| 2671 | let mut editor = CanvasEditor::from_page(reread, &mut engine).unwrap(); |
| 2672 | editor.focus_outline(body).unwrap(); |
| 2673 | editor.select(selection).unwrap(); |
| 2674 | assert_eq!(editor.format_state().unwrap(), state); |
| 2675 | assert_eq!( |
| 2676 | state, |
| 2677 | FormatState { |
| 2678 | toggles: vec![Toggle::Bold, Toggle::Superscript], |
| 2679 | font: state.font.clone(), |
| 2680 | font_size: Some(14.0), |
| 2681 | alignment: Some(Alignment::Center), |
| 2682 | bullets: false, |
| 2683 | numbering: true, |
| 2684 | listed: true, |
| 2685 | list: Some(ListStyle::NUMBER), |
| 2686 | tags: vec![ |
| 2687 | (NoteTag::defaults()[0].clone(), 0), |
| 2688 | (NoteTag::defaults()[2].clone(), 2) |
| 2689 | ], |
| 2690 | style: state.style.clone(), |
| 2691 | style_font: state.style_font.clone(), |
| 2692 | style_size: state.style_size, |
| 2693 | } |
| 2694 | ); |
| 2695 | // Format Painter, a gallery tag and an inserted table write as well. |
| 2696 | let painted = editor.painted_format().unwrap(); |
| 2697 | editor.select([at(0, 0), at(0, 2)].into()).unwrap(); |
| 2698 | editor |
| 2699 | .format(&mut engine, Formatting::Paint(painted)) |
| 2700 | .unwrap(); |
| 2701 | let state = editor.format_state().unwrap(); |
| 2702 | assert_eq!( |
| 2703 | (state.toggles, state.font_size, state.alignment), |
| 2704 | ( |
| 2705 | vec![Toggle::Bold, Toggle::Superscript], |
| 2706 | Some(14.0), |
| 2707 | Some(Alignment::Center) |
| 2708 | ) |
| 2709 | ); |
| 2710 | editor |
| 2711 | .format( |
| 2712 | &mut engine, |
| 2713 | Formatting::Tag(NoteTag::defaults()[25].clone(), 25), |
| 2714 | ) |
| 2715 | .unwrap(); |
| 2716 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 2717 | editor.insert_table(&mut engine, 2, 3).unwrap(); |
| 2718 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); |
| 2719 | let edited = settled(editor.page().unwrap()); |
| 2720 | let written = super::ops::saved(&written, space, &mut editor); |
| 2721 | let (_, reread) = page(&written); |
| 2722 | assert_eq!(settled(reread).objects, edited.objects); |
| 2723 | } |
| 2724 | } |