| author | |
| committer | |
| log | 91a2bf8cea7dd59ae56add4cee8bb968e653a439 |
| tree | 5282a37dbfda458e3174e9d68faac4001071af42 |
| parent | f4e1afda77c93656bcf5c1e0e634aa9ca735d9ec |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
- File attachments (#23): Attach File, drag and drop, open, and Save As, stored
inline with a file-type icon as OneNote 2010 does; files on blank page are
page-level objects that move, delete, and sync.
- Customize Tags (#8): OneNote's dialog with all 143 symbols and both palettes;
the tag list lives in settings and drives the toolbar, menus, palette, and
shortcuts; tag definitions store OneNote's status bits.
- Undo groups a run of typing until the caret moves or another edit, as
OneNote 2010 does, on desktop and iOS.
- Background art is one undoable editor step that renders on the next frame,
with an opaque backing that hides rule lines as OneNote's art does.
- Renderer: shadows are computed in the shader and the icon atlas evicts and
grows instead of failing a frame.
- iOS: sync status and sheet, tags with the new artwork, Tags Summary, page
background, cross-notebook search, and a replica creation race fix.
- An unset paragraph language is no longer written as language 0.
Assisted-by: claude-opus-5.5315 files changed, 10688 insertions(+), 1481 deletions(-)
Cargo.lock+1| ... | ... | @@ -1888,6 +1888,7 @@ dependencies = [ |
| 1888 | 1888 | "onestore", |
| 1889 | 1889 | "parley", |
| 1890 | 1890 | "pollster", |
| 1891 | "resvg", | |
| 1891 | 1892 | "serde", |
| 1892 | 1893 | "serde_json", |
| 1893 | 1894 | "tempfile", |
apps/ios/Snowbound/App.swift+33-11| ... | ... | @@ -19,6 +19,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { |
| 19 | 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? |
| 20 | 20 | ) -> Bool { |
| 21 | 21 | sb_set_coordinator(coordinate) |
| 22 | sb_set_sync_wake { DispatchQueue.main.async { NotificationCenter.default.post(name: Sync.changed, object: nil) } } | |
| 22 | 23 | #if DEBUG |
| 23 | 24 | Presenter.watch() |
| 24 | 25 | #endif |
| ... | ... | @@ -77,11 +78,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 77 | 78 | split.setViewController(pages, for: .supplementary) |
| 78 | 79 | split.setViewController(UIViewController(), for: .secondary) |
| 79 | 80 | notebooks.onOpen = { [weak self] tab, notebook in self?.open(tab, of: notebook) } |
| 80 | pages.onOpen = { [weak self] section, id, find in self?.show(section: section, page: id, find: find) } | |
| 81 | pages.onOpenSection = { [weak self] notebook, path, id, find in | |
| 82 | guard let tab = notebook.tabs.first(where: { $0.path == path }) else { return } | |
| 83 | self?.open(tab, of: notebook, page: id, find: find) | |
| 84 | } | |
| 81 | pages.onOpen = { [weak self] section, id in self?.show(section: section, page: id) } | |
| 85 | 82 | window.rootViewController = split |
| 86 | 83 | window.makeKeyAndVisible() |
| 87 | 84 | self.window = window |
| ... | ... | @@ -116,11 +113,13 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 116 | 113 | } |
| 117 | 114 | |
| 118 | 115 | func open( |
| 119 | _ tab: Tab, of notebook: Notebook, page: String? = nil, find: String? = nil, | |
| 116 | _ tab: Tab, of notebook: Notebook, page: String? = nil, reveal: Reveal? = nil, | |
| 120 | 117 | then: ((Section) -> Void)? = nil |
| 121 | 118 | ) { |
| 122 | 119 | guard Author.name != nil else { |
| 123 | return Author.ask(from: split) { [weak self] in self?.open(tab, of: notebook, page: page, find: find, then: then) } | |
| 120 | return Author.ask(from: split) { [weak self] in | |
| 121 | self?.open(tab, of: notebook, page: page, reveal: reveal, then: then) | |
| 122 | } | |
| 124 | 123 | } |
| 125 | 124 | pages.loading(tab) |
| 126 | 125 | split.show(.supplementary) |
| ... | ... | @@ -134,15 +133,37 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 134 | 133 | notebooks.select(tab, of: notebook) |
| 135 | 134 | Place.saved = Place(notebook: notebook.id, section: tab.path, page: page) |
| 136 | 135 | if let page, section.row(of: page) != nil { |
| 137 | show(section: section, page: page, find: find) | |
| 136 | show(section: section, page: page, reveal: reveal) | |
| 138 | 137 | } |
| 139 | 138 | then?(section) |
| 140 | 139 | } |
| 141 | 140 | } |
| 142 | 141 | |
| 143 | func show(section: Section, page: String, find: String? = nil, titleFocus: Bool = false) { | |
| 142 | /// Opens `page` of the section at catalog `path` in `notebook`, as a search result or | |
| 143 | /// the Tags Summary leads there. | |
| 144 | func open(_ path: String, of notebook: Notebook, page: String, reveal: Reveal) { | |
| 145 | if let section = pages.section, section.notebook === notebook, section.tab.path == path { | |
| 146 | return show(section: section, page: page, reveal: reveal) | |
| 147 | } | |
| 148 | guard let tab = notebook.tabs.first(where: { $0.path == path }) else { return } | |
| 149 | open(tab, of: notebook, page: page, reveal: reveal) | |
| 150 | } | |
| 151 | ||
| 152 | /// OneNote's Tags Summary for `notebook`, as a sheet over `controller`. | |
| 153 | func showTags(of notebook: Notebook, from controller: UIViewController) { | |
| 154 | let tags = TagsViewController(notebook: notebook) | |
| 155 | tags.onOpen = { [weak self] notebook, path, page, paragraph in | |
| 156 | self?.open(path, of: notebook, page: page, reveal: .paragraph(paragraph)) | |
| 157 | } | |
| 158 | let navigation = UINavigationController(rootViewController: tags) | |
| 159 | navigation.sheetPresentationController?.detents = [.medium(), .large()] | |
| 160 | navigation.sheetPresentationController?.prefersGrabberVisible = true | |
| 161 | controller.present(navigation, animated: true) | |
| 162 | } | |
| 163 | ||
| 164 | func show(section: Section, page: String, reveal: Reveal? = nil, titleFocus: Bool = false) { | |
| 144 | 165 | showingPage = true |
| 145 | let controller = PageViewController(section: section, page: page, find: find, titleFocus: titleFocus) | |
| 166 | let controller = PageViewController(section: section, page: page, reveal: reveal, titleFocus: titleFocus) | |
| 146 | 167 | controller.onOpen = { [weak self] id in self?.show(section: section, page: id) } |
| 147 | 168 | split.setViewController(UINavigationController(rootViewController: controller), for: .secondary) |
| 148 | 169 | split.show(.secondary) |
| ... | ... | @@ -204,7 +225,8 @@ final class RootViewController: UISplitViewController { |
| 204 | 225 | [ |
| 205 | 226 | UIKeyCommand(title: "New Page", action: #selector(newPage), input: "n", modifierFlags: .command), |
| 206 | 227 | UIKeyCommand( |
| 207 | title: "New Subpage", action: #selector(newSubpage), input: "n", modifierFlags: [.command, .shift]), | |
| 228 | title: "New Subpage", action: #selector(newSubpage), input: "n", | |
| 229 | modifierFlags: [.command, .shift, .alternate]), | |
| 208 | 230 | UIKeyCommand(title: "Search", action: #selector(search), input: "f", modifierFlags: [.command, .alternate]), |
| 209 | 231 | ] |
| 210 | 232 | } |
apps/ios/Snowbound/CanvasView.swift+151-19| ... | ... | @@ -106,6 +106,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 106 | 106 | /// gestures move; the canvas paints neither. |
| 107 | 107 | private lazy var display = UITextSelectionDisplayInteraction(textInput: self, delegate: self) |
| 108 | 108 | private let history = CanvasUndoManager() |
| 109 | private let spaceHint = Hint() | |
| 109 | 110 | |
| 110 | 111 | weak var inputDelegate: UITextInputDelegate? |
| 111 | 112 | lazy var tokenizer: UITextInputTokenizer = LineTokenizer(canvas: self) |
| ... | ... | @@ -134,6 +135,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 134 | 135 | minimumZoomScale = 0.25 |
| 135 | 136 | maximumZoomScale = 4 |
| 136 | 137 | keyboardDismissMode = .interactive |
| 138 | // A phone on its side keeps the page clear of the sensor housing, as it keeps the top. | |
| 139 | contentInsetAdjustmentBehavior = .always | |
| 137 | 140 | addSubview(content) |
| 138 | 141 | insertSubview(metal, at: 0) |
| 139 | 142 | // Touches land on the scroll view itself, where the text interaction looks for them. |
| ... | ... | @@ -487,6 +490,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 487 | 490 | default: |
| 488 | 491 | display.isActivated = isFirstResponder |
| 489 | 492 | edit(external: true) { sb_view_release(handle) } |
| 493 | spaceHint.hide() | |
| 490 | 494 | } |
| 491 | 495 | } |
| 492 | 496 | |
| ... | ... | @@ -512,7 +516,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 512 | 516 | |
| 513 | 517 | /// Tells the bar buttons and the format bar what the selection now has. |
| 514 | 518 | private func changedSelection() { |
| 515 | if let handle { formatBar.show(sb_view_format(handle)) } | |
| 519 | formatBar.show(format) | |
| 516 | 520 | onChange?() |
| 517 | 521 | } |
| 518 | 522 | |
| ... | ... | @@ -576,14 +580,20 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 576 | 580 | } |
| 577 | 581 | } |
| 578 | 582 | |
| 583 | /// A page selection copies every outline, as OneNote 2010 copies it. | |
| 579 | 584 | override func copy(_ sender: Any?) { |
| 580 | guard let range = selectedTextRange, let text = text(in: range) else { return } | |
| 585 | guard let handle, let text = take(sb_view_copy(handle, false)) else { return } | |
| 581 | 586 | UIPasteboard.general.string = text |
| 582 | 587 | } |
| 583 | 588 | |
| 584 | 589 | override func cut(_ sender: Any?) { |
| 585 | copy(sender) | |
| 586 | delete(sender) | |
| 590 | guard let handle else { return } | |
| 591 | var text: String? | |
| 592 | edit(external: true) { | |
| 593 | text = take(sb_view_copy(handle, true)) | |
| 594 | return text != nil | |
| 595 | } | |
| 596 | if let text { UIPasteboard.general.string = text } | |
| 587 | 597 | } |
| 588 | 598 | |
| 589 | 599 | override func delete(_ sender: Any?) { |
| ... | ... | @@ -614,6 +624,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 614 | 624 | override func toggleItalics(_ sender: Any?) { apply(1) } |
| 615 | 625 | override func toggleUnderline(_ sender: Any?) { apply(2) } |
| 616 | 626 | |
| 627 | /// Tags and the highlighter join the system's text actions. | |
| 628 | func editMenu(for textRange: UITextRange, suggestedActions: [UIMenuElement]) -> UIMenu? { | |
| 629 | guard let handle else { return nil } | |
| 630 | let bits = sb_view_format(handle) | |
| 631 | let tag = UIMenu( | |
| 632 | title: "Tag", image: UIImage(systemName: "tag"), | |
| 633 | children: Tags.menu(bits: bits) { [weak self] in self?.apply($0) }) | |
| 634 | var extra: [UIMenuElement] = [tag] | |
| 635 | if !textRange.isEmpty { | |
| 636 | extra.insert( | |
| 637 | UIAction(title: "Highlight", image: UIImage(systemName: "highlighter")) { [weak self] _ in | |
| 638 | self?.apply(10) | |
| 639 | }, at: 0) | |
| 640 | } | |
| 641 | return UIMenu(children: suggestedActions + [UIMenu(options: .displayInline, children: extra)]) | |
| 642 | } | |
| 643 | ||
| 617 | 644 | // MARK: Page |
| 618 | 645 | |
| 619 | 646 | /// A conflict page, which shows what is stored and takes no edits. |
| ... | ... | @@ -624,6 +651,55 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 624 | 651 | |
| 625 | 652 | var pageText: String { handle.flatMap { take(sb_view_page_text($0)) } ?? "" } |
| 626 | 653 | |
| 654 | /// The selection's formatting, from `sb_view_format`. | |
| 655 | var format: UInt64 { handle.map(sb_view_format) ?? 0 } | |
| 656 | ||
| 657 | var pagePaper: Paper? { handle.flatMap { decode(Paper.self, sb_view_paper($0)) } } | |
| 658 | ||
| 659 | /// Gives the page colour `color` and rule lines `ruled` of `paper.rules`, or none. | |
| 660 | func setPaper(color: [UInt8]?, ruled: Int?) { | |
| 661 | guard let handle else { return } | |
| 662 | edit(external: true) { | |
| 663 | sb_view_set_paper( | |
| 664 | handle, color.map { Int16($0[0]) } ?? -1, color?[1] ?? 0, color?[2] ?? 0, Int8(ruled ?? -1)) | |
| 665 | } | |
| 666 | } | |
| 667 | ||
| 668 | /// Puts template `name`'s art behind the page, or none. | |
| 669 | func setArt(_ name: String?) { | |
| 670 | guard let handle else { return } | |
| 671 | edit(external: true) { sb_view_set_art(handle, name) } | |
| 672 | } | |
| 673 | ||
| 674 | /// Insert Space: the next drag moves what lies below or right of where it starts. | |
| 675 | func insertSpace() { | |
| 676 | guard let handle else { return } | |
| 677 | sb_view_insert_space(handle) | |
| 678 | spaceHint.show("Drag down or right to add space", in: self) | |
| 679 | } | |
| 680 | ||
| 681 | /// Types the date, time or both as the system writes them short, and a space, as | |
| 682 | /// OneNote's Insert Date and Time do. | |
| 683 | func insertDate(_ date: Bool, time: Bool) { | |
| 684 | let now = Date() | |
| 685 | let text = DateFormatter.localizedString( | |
| 686 | from: now, dateStyle: date ? .short : .none, timeStyle: time ? .short : .none) | |
| 687 | if !isFirstResponder { _ = becomeFirstResponder() } | |
| 688 | insertText(text + " ") | |
| 689 | } | |
| 690 | ||
| 691 | /// Selects paragraph `id` and brings it into view. | |
| 692 | func selectParagraph(_ id: String) -> Bool { | |
| 693 | guard let handle else { return false } | |
| 694 | var found = false | |
| 695 | edit(external: true) { | |
| 696 | found = sb_view_select_paragraph(handle, id) | |
| 697 | return found | |
| 698 | } | |
| 699 | if found { reveal(selectedTextRange.map { firstRect(for: $0) } ?? .zero) } | |
| 700 | return found | |
| 701 | } | |
| 702 | ||
| 627 | 703 | /// Applies `sb_view_apply` formatting to the selection. |
| 628 | 704 | func apply(_ command: UInt8) { |
| 629 | 705 | guard let handle else { return } |
| ... | ... | @@ -682,26 +758,38 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 682 | 758 | override var keyCommands: [UIKeyCommand]? { |
| 683 | 759 | guard isFirstResponder else { return nil } |
| 684 | 760 | // Ctrl+1 to Ctrl+9 in OneNote, where the Mac's Command stands for Control. |
| 685 | let tags = FormatBar.tags.enumerated().map { index, name in | |
| 686 | let command = UIKeyCommand( | |
| 687 | title: name, action: #selector(tagged), input: "\(index + 1)", modifierFlags: .command, | |
| 688 | propertyList: index) | |
| 689 | command.wantsPriorityOverSystemBehavior = true | |
| 690 | return command | |
| 691 | } | |
| 692 | let outdent = UIKeyCommand( | |
| 693 | title: "Decrease Indent", action: #selector(outdented), input: "\t", modifierFlags: .shift) | |
| 694 | outdent.wantsPriorityOverSystemBehavior = true | |
| 761 | let tags = Tags.all.prefix(Tags.keyed).enumerated().map { index, tag in | |
| 762 | UIKeyCommand( | |
| 763 | title: tag.name, action: #selector(formatted), input: "\(index + 1)", modifierFlags: .command, | |
| 764 | propertyList: 16 + index) | |
| 765 | } | |
| 766 | // The desktop's chords, as OneNote for Mac's. | |
| 767 | let formats: [(String, String, UIKeyModifierFlags, Int)] = [ | |
| 768 | ("Strikethrough", "x", [.command, .shift], 3), | |
| 769 | ("Bullets", ".", .command, 4), | |
| 770 | ("Numbering", "/", .command, 5), | |
| 771 | ("Increase Indent", "]", .command, 6), | |
| 772 | ("Decrease Indent", "[", .command, 7), | |
| 773 | ("Remove Tag", "0", [.command, .control], 8), | |
| 774 | ("Clear Formatting", "n", [.command, .shift], 9), | |
| 775 | ("Highlight", "h", [.command, .control], 10), | |
| 776 | ("Decrease Indent", "\t", .shift, 7), | |
| 777 | ] | |
| 778 | let commands = tags + formats.map { title, input, modifiers, command in | |
| 779 | UIKeyCommand( | |
| 780 | title: title, action: #selector(formatted), input: input, modifierFlags: modifiers, | |
| 781 | propertyList: command) | |
| 782 | } | |
| 783 | for command in commands { command.wantsPriorityOverSystemBehavior = true } | |
| 695 | 784 | let done = UIKeyCommand(title: "Done", action: #selector(finished), input: UIKeyCommand.inputEscape) |
| 696 | return tags + [outdent, done] | |
| 785 | return commands + [done] | |
| 697 | 786 | } |
| 698 | 787 | |
| 699 | @objc private func tagged(_ command: UIKeyCommand) { | |
| 700 | guard let index = command.propertyList as? Int else { return } | |
| 701 | apply(16 + UInt8(index)) | |
| 788 | @objc private func formatted(_ command: UIKeyCommand) { | |
| 789 | guard let command = command.propertyList as? Int else { return } | |
| 790 | apply(UInt8(command)) | |
| 702 | 791 | } |
| 703 | 792 | |
| 704 | @objc private func outdented() { apply(7) } | |
| 705 | 793 | @objc private func finished() { _ = resignFirstResponder() } |
| 706 | 794 | |
| 707 | 795 | // MARK: Coordinates |
| ... | ... | @@ -948,3 +1036,47 @@ private final class LineTokenizer: UITextInputStringTokenizer { |
| 948 | 1036 | granularity == .line || super.isPosition(position, withinTextUnit: granularity, inDirection: direction) |
| 949 | 1037 | } |
| 950 | 1038 | } |
| 1039 | ||
| 1040 | /// A short instruction floating at the bottom of the page until the gesture it asks for. | |
| 1041 | private final class Hint: UIVisualEffectView { | |
| 1042 | private let label = UILabel() | |
| 1043 | ||
| 1044 | init() { | |
| 1045 | super.init(effect: UIBlurEffect(style: .systemThickMaterial)) | |
| 1046 | label.font = .preferredFont(forTextStyle: .subheadline) | |
| 1047 | label.textAlignment = .center | |
| 1048 | label.translatesAutoresizingMaskIntoConstraints = false | |
| 1049 | contentView.addSubview(label) | |
| 1050 | NSLayoutConstraint.activate([ | |
| 1051 | label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), | |
| 1052 | label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), | |
| 1053 | label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), | |
| 1054 | label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), | |
| 1055 | ]) | |
| 1056 | layer.cornerRadius = 18 | |
| 1057 | clipsToBounds = true | |
| 1058 | isUserInteractionEnabled = false | |
| 1059 | } | |
| 1060 | ||
| 1061 | required init?(coder: NSCoder) { fatalError() } | |
| 1062 | ||
| 1063 | /// Shows `text` over the bottom of `view`'s visible area, and says it. | |
| 1064 | func show(_ text: String, in view: UIScrollView) { | |
| 1065 | label.text = text | |
| 1066 | guard let host = view.superview else { return } | |
| 1067 | translatesAutoresizingMaskIntoConstraints = false | |
| 1068 | host.addSubview(self) | |
| 1069 | NSLayoutConstraint.activate([ | |
| 1070 | centerXAnchor.constraint(equalTo: host.centerXAnchor), | |
| 1071 | bottomAnchor.constraint(equalTo: host.keyboardLayoutGuide.topAnchor, constant: -16), | |
| 1072 | ]) | |
| 1073 | alpha = 0 | |
| 1074 | UIView.animate(withDuration: 0.2) { self.alpha = 1 } | |
| 1075 | UIAccessibility.post(notification: .announcement, argument: text) | |
| 1076 | } | |
| 1077 | ||
| 1078 | func hide() { | |
| 1079 | guard superview != nil else { return } | |
| 1080 | UIView.animate(withDuration: 0.2, animations: { self.alpha = 0 }) { _ in self.removeFromSuperview() } | |
| 1081 | } | |
| 1082 | } |
apps/ios/Snowbound/FormatBar.swift+12-13| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | import UIKit |
| 2 | 2 | |
| 3 | /// Formatting above the keyboard, as Notes offers it: tags and lists first, then text | |
| 4 | /// styles, indentation, tags and pictures, the keyboard's dismissal at the end. | |
| 3 | /// Formatting above the keyboard, as Notes offers it: To Do first, then text styles, lists, | |
| 4 | /// indentation, tags and pictures, the keyboard's dismissal at the end. | |
| 5 | 5 | final class FormatBar: UIInputView { |
| 6 | 6 | /// `sb_view_apply` commands and `sb_view_format` bits. |
| 7 | 7 | private static let buttons: [(symbol: String, label: String, command: UInt8)] = [ |
| ... | ... | @@ -10,18 +10,14 @@ final class FormatBar: UIInputView { |
| 10 | 10 | ("italic", "Italic", 1), |
| 11 | 11 | ("underline", "Underline", 2), |
| 12 | 12 | ("strikethrough", "Strikethrough", 3), |
| 13 | ("highlighter", "Highlight", 10), | |
| 13 | 14 | ("list.bullet", "Bullets", 4), |
| 14 | 15 | ("list.number", "Numbering", 5), |
| 15 | 16 | ("decrease.indent", "Decrease Indent", 7), |
| 16 | 17 | ("increase.indent", "Increase Indent", 6), |
| 17 | 18 | ] |
| 18 | /// OneNote 2010's default tags in their order, Ctrl+1 to Ctrl+9. | |
| 19 | static let tags = [ | |
| 20 | "To Do", "Important", "Question", "Remember for Later", "Definition", "Highlight", "Contact", "Address", | |
| 21 | "Phone Number", | |
| 22 | ] | |
| 23 | ||
| 24 | 19 | private var toggles: [(UIButton, UInt8)] = [] |
| 20 | private var bits: UInt64 = 0 | |
| 25 | 21 | var onApply: ((UInt8) -> Void)? |
| 26 | 22 | /// Asks for a picture from the camera (true) or the photo library. |
| 27 | 23 | var onPicture: ((Bool) -> Void)? |
| ... | ... | @@ -44,9 +40,11 @@ final class FormatBar: UIInputView { |
| 44 | 40 | row.addArrangedSubview(button) |
| 45 | 41 | } |
| 46 | 42 | let tag = Self.button("tag", "Tags") |
| 47 | tag.menu = UIMenu(children: Self.tags.enumerated().map { index, name in | |
| 48 | UIAction(title: name) { [weak self] _ in self?.onApply?(16 + UInt8(index)) } | |
| 49 | }) | |
| 43 | tag.menu = UIMenu(children: [ | |
| 44 | UIDeferredMenuElement.uncached { [weak self] done in | |
| 45 | done(Tags.menu(bits: self?.bits ?? 0) { self?.onApply?($0) }) | |
| 46 | } | |
| 47 | ]) | |
| 50 | 48 | tag.showsMenuAsPrimaryAction = true |
| 51 | 49 | row.addArrangedSubview(tag) |
| 52 | 50 | let picture = Self.button("photo", "Insert Picture") |
| ... | ... | @@ -105,9 +103,10 @@ final class FormatBar: UIInputView { |
| 105 | 103 | } |
| 106 | 104 | |
| 107 | 105 | /// Marks what the selection has, from `sb_view_format`. |
| 108 | func show(_ bits: UInt32) { | |
| 106 | func show(_ bits: UInt64) { | |
| 107 | self.bits = bits | |
| 109 | 108 | for (button, command) in toggles { |
| 110 | button.isSelected = bits & (1 << UInt32(command)) != 0 | |
| 109 | button.isSelected = bits & (1 << UInt64(command)) != 0 | |
| 111 | 110 | } |
| 112 | 111 | } |
| 113 | 112 | } |
apps/ios/Snowbound/Lists.swift+83-65| ... | ... | @@ -39,11 +39,14 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele |
| 39 | 39 | ]) |
| 40 | 40 | navigationItem.rightBarButtonItem = UIBarButtonItem( |
| 41 | 41 | title: "Add Notebook", image: UIImage(systemName: "plus"), menu: add) |
| 42 | navigationItem.searchController = SearchViewController.controller() | |
| 43 | toolbarItems = [.flexibleSpace(), UIBarButtonItem(customView: SyncIndicator()), .flexibleSpace()] | |
| 42 | 44 | } |
| 43 | 45 | |
| 44 | 46 | override func viewWillAppear(_ animated: Bool) { |
| 45 | 47 | super.viewWillAppear(animated) |
| 46 | 48 | navigationController?.navigationBar.prefersLargeTitles = true |
| 49 | navigationController?.setToolbarHidden(false, animated: false) | |
| 47 | 50 | } |
| 48 | 51 | |
| 49 | 52 | func reload() { |
| ... | ... | @@ -170,6 +173,10 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele |
| 170 | 173 | UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) { |
| 171 | 174 | [weak self] _ in self?.newSection(in: notebook) |
| 172 | 175 | }, |
| 176 | UIAction(title: "Tags Summary", image: UIImage(systemName: "tag")) { [weak self] _ in | |
| 177 | guard let self else { return } | |
| 178 | (view.window?.windowScene?.delegate as? SceneDelegate)?.showTags(of: notebook, from: self) | |
| 179 | }, | |
| 173 | 180 | UIAction(title: "Close Notebook", image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { |
| 174 | 181 | [weak self] _ in |
| 175 | 182 | Notebooks.remove(notebook) |
| ... | ... | @@ -250,7 +257,7 @@ extension UIFont { |
| 250 | 257 | } |
| 251 | 258 | |
| 252 | 259 | /// A section's pages, subpages indented, each page's conflict pages beneath it. |
| 253 | final class PagesViewController: UITableViewController, UISearchResultsUpdating { | |
| 260 | final class PagesViewController: UITableViewController { | |
| 254 | 261 | private struct Item { |
| 255 | 262 | let row: Row |
| 256 | 263 | let version: Row.Version? |
| ... | ... | @@ -260,13 +267,9 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 260 | 267 | private(set) var section: Section? |
| 261 | 268 | private var items: [Item] = [] |
| 262 | 269 | private var selected: String? |
| 263 | private let results = SearchViewController() | |
| 264 | private lazy var search = UISearchController(searchResultsController: results) | |
| 265 | private var searching: DispatchWorkItem? | |
| 270 | private let search = SearchViewController.controller() | |
| 266 | 271 | /// Opens a page of the section. |
| 267 | var onOpen: ((Section, String, String?) -> Void)? | |
| 268 | /// Opens a page of another section of the notebook, found by search. | |
| 269 | var onOpenSection: ((Notebook, String, String, String?) -> Void)? | |
| 272 | var onOpen: ((Section, String) -> Void)? | |
| 270 | 273 | |
| 271 | 274 | init() { |
| 272 | 275 | super.init(style: .plain) |
| ... | ... | @@ -277,22 +280,14 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 277 | 280 | override func viewDidLoad() { |
| 278 | 281 | super.viewDidLoad() |
| 279 | 282 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") |
| 280 | search.searchResultsUpdater = self | |
| 281 | search.searchBar.placeholder = "Search Notebook" | |
| 282 | 283 | navigationItem.searchController = search |
| 283 | 284 | navigationItem.hidesSearchBarWhenScrolling = false |
| 284 | results.onOpen = { [weak self] found, query in | |
| 285 | guard let self, let section, let notebook = Optional(section.notebook) else { return } | |
| 286 | if found.section == section.tab.path { | |
| 287 | onOpen?(section, found.page, query) | |
| 288 | } else { | |
| 289 | onOpenSection?(notebook, found.section, found.page, query) | |
| 290 | } | |
| 291 | } | |
| 292 | 285 | let compose = UIBarButtonItem( |
| 293 | 286 | title: "New Page", image: UIImage(systemName: "square.and.pencil"), |
| 294 | 287 | primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) }) |
| 295 | toolbarItems = [.flexibleSpace(), compose] | |
| 288 | toolbarItems = [ | |
| 289 | .flexibleSpace(), UIBarButtonItem(customView: SyncIndicator()), .flexibleSpace(), compose, | |
| 290 | ] | |
| 296 | 291 | NotificationCenter.default.addObserver( |
| 297 | 292 | self, selector: #selector(changed), name: Section.changed, object: nil) |
| 298 | 293 | } |
| ... | ... | @@ -398,7 +393,7 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 398 | 393 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| 399 | 394 | guard let section else { return } |
| 400 | 395 | selected = items[indexPath.row].id |
| 401 | onOpen?(section, items[indexPath.row].id, nil) | |
| 396 | onOpen?(section, items[indexPath.row].id) | |
| 402 | 397 | } |
| 403 | 398 | |
| 404 | 399 | override func tableView( |
| ... | ... | @@ -432,34 +427,6 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 432 | 427 | return UIMenu(children: actions) |
| 433 | 428 | }) |
| 434 | 429 | } |
| 435 | ||
| 436 | func updateSearchResults(for controller: UISearchController) { | |
| 437 | searching?.cancel() | |
| 438 | let query = controller.searchBar.text?.trimmingCharacters(in: .whitespaces) ?? "" | |
| 439 | guard let section, let library = section.notebook.handle, !query.isEmpty else { | |
| 440 | results.show([], query: query) | |
| 441 | return | |
| 442 | } | |
| 443 | // Each keystroke waits for the next before the notebook is read. | |
| 444 | let work = DispatchWorkItem { [weak self] in | |
| 445 | let (library, open, path) = (Int(bitPattern: library), Int(bitPattern: section.handle), section.tab.path) | |
| 446 | self?.results.searching(query) | |
| 447 | background({ | |
| 448 | decode( | |
| 449 | [Found].self, | |
| 450 | sb_library_search( | |
| 451 | OpaquePointer(bitPattern: library), OpaquePointer(bitPattern: open), path, query)) ?? [] | |
| 452 | }) { [weak self] found in | |
| 453 | guard controller.searchBar.text?.trimmingCharacters(in: .whitespaces) == query else { return } | |
| 454 | self?.results.show( | |
| 455 | found, query: query, | |
| 456 | sections: Dictionary( | |
| 457 | section.notebook.tabs.map { ($0.path, $0.name) }, uniquingKeysWith: { first, _ in first })) | |
| 458 | } | |
| 459 | } | |
| 460 | searching = work | |
| 461 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work) | |
| 462 | } | |
| 463 | 430 | } |
| 464 | 431 | |
| 465 | 432 | /// A page `sb_library_search` found. |
| ... | ... | @@ -470,42 +437,90 @@ struct Found: Decodable { |
| 470 | 437 | let snippet: String |
| 471 | 438 | } |
| 472 | 439 | |
| 473 | final class SearchViewController: UITableViewController { | |
| 474 | private var found: [Found] = [] | |
| 475 | private var sections: [String: String] = [:] | |
| 440 | /// Every open notebook's pages holding a query, grouped by notebook, as Notes searches | |
| 441 | /// every folder. | |
| 442 | final class SearchViewController: UITableViewController, UISearchResultsUpdating { | |
| 443 | private var found: [(notebook: Notebook, pages: [Found])] = [] | |
| 476 | 444 | private var query = "" |
| 477 | var onOpen: ((Found, String) -> Void)? | |
| 445 | private var searching: DispatchWorkItem? | |
| 446 | ||
| 447 | /// A search bar whose results this shows. | |
| 448 | static func controller() -> UISearchController { | |
| 449 | let results = SearchViewController(style: .insetGrouped) | |
| 450 | let search = UISearchController(searchResultsController: results) | |
| 451 | search.searchResultsUpdater = results | |
| 452 | search.searchBar.placeholder = "Search Notebooks" | |
| 453 | return search | |
| 454 | } | |
| 478 | 455 | |
| 479 | 456 | override func viewDidLoad() { |
| 480 | 457 | super.viewDidLoad() |
| 481 | 458 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "found") |
| 482 | 459 | } |
| 483 | 460 | |
| 484 | func searching(_ query: String) { | |
| 485 | guard found.isEmpty else { return } | |
| 486 | var loading = UIContentUnavailableConfiguration.loading() | |
| 487 | loading.text = "Searching…" | |
| 488 | contentUnavailableConfiguration = loading | |
| 461 | func updateSearchResults(for controller: UISearchController) { | |
| 462 | searching?.cancel() | |
| 463 | let query = controller.searchBar.text?.trimmingCharacters(in: .whitespaces) ?? "" | |
| 464 | guard !query.isEmpty else { return show([], query: query) } | |
| 465 | // Each keystroke waits for the next before the notebooks are read. | |
| 466 | let work = DispatchWorkItem { [weak self] in | |
| 467 | if self?.found.isEmpty == true { | |
| 468 | var loading = UIContentUnavailableConfiguration.loading() | |
| 469 | loading.text = "Searching…" | |
| 470 | self?.contentUnavailableConfiguration = loading | |
| 471 | } | |
| 472 | let notebooks = Notebooks.all.compactMap { notebook -> (Notebook, Int, Int, String?)? in | |
| 473 | guard let library = notebook.handle else { return nil } | |
| 474 | // Open sections are searched as their edits leave them. | |
| 475 | let open = Section.all.first { $0.notebook === notebook } | |
| 476 | return (notebook, Int(bitPattern: library), open.map { Int(bitPattern: $0.handle) } ?? 0, open?.tab.path) | |
| 477 | } | |
| 478 | background({ | |
| 479 | notebooks.map { notebook, library, open, path in | |
| 480 | let found = decode( | |
| 481 | [Found].self, | |
| 482 | sb_library_search( | |
| 483 | OpaquePointer(bitPattern: library), OpaquePointer(bitPattern: open), path, query)) | |
| 484 | return (notebook: notebook, pages: found ?? []) | |
| 485 | } | |
| 486 | }) { [weak self] found in | |
| 487 | guard controller.searchBar.text?.trimmingCharacters(in: .whitespaces) == query else { return } | |
| 488 | self?.show(found.filter { !$0.pages.isEmpty }, query: query) | |
| 489 | } | |
| 490 | } | |
| 491 | searching = work | |
| 492 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work) | |
| 489 | 493 | } |
| 490 | 494 | |
| 491 | func show(_ found: [Found], query: String, sections: [String: String] = [:]) { | |
| 495 | private func show(_ found: [(notebook: Notebook, pages: [Found])], query: String) { | |
| 492 | 496 | self.found = found |
| 493 | 497 | self.query = query |
| 494 | self.sections = sections | |
| 495 | 498 | tableView.reloadData() |
| 496 | contentUnavailableConfiguration = found.isEmpty && !query.isEmpty ? UIContentUnavailableConfiguration.search() : nil | |
| 499 | contentUnavailableConfiguration = | |
| 500 | found.isEmpty && !query.isEmpty ? UIContentUnavailableConfiguration.search() : nil | |
| 497 | 501 | } |
| 498 | 502 | |
| 499 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { found.count } | |
| 503 | override func numberOfSections(in tableView: UITableView) -> Int { found.count } | |
| 504 | ||
| 505 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
| 506 | found[section].notebook.name | |
| 507 | } | |
| 508 | ||
| 509 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
| 510 | found[section].pages.count | |
| 511 | } | |
| 500 | 512 | |
| 501 | 513 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
| 502 | let found = found[indexPath.row] | |
| 514 | let (notebook, pages) = found[indexPath.section] | |
| 515 | let page = pages[indexPath.row] | |
| 503 | 516 | let cell = tableView.dequeueReusableCell(withIdentifier: "found", for: indexPath) |
| 504 | 517 | var content = UIListContentConfiguration.subtitleCell() |
| 505 | content.text = found.title.isEmpty ? "Untitled Page" : found.title | |
| 506 | let section = sections[found.section] ?? "" | |
| 518 | content.text = page.title.isEmpty ? "Untitled Page" : page.title | |
| 519 | let tab = notebook.tabs.first { $0.path == page.section } | |
| 520 | content.image = UIImage(systemName: "rectangle.portrait.fill") | |
| 521 | content.imageProperties.tintColor = tab?.uiColor ?? .systemGray | |
| 507 | 522 | content.secondaryAttributedText = highlighted( |
| 508 | [section, found.snippet].filter { !$0.isEmpty }.joined(separator: " · ")) | |
| 523 | [tab?.name ?? "", page.snippet].filter { !$0.isEmpty }.joined(separator: " · ")) | |
| 509 | 524 | content.secondaryTextProperties.numberOfLines = 2 |
| 510 | 525 | cell.contentConfiguration = content |
| 511 | 526 | return cell |
| ... | ... | @@ -526,6 +541,9 @@ final class SearchViewController: UITableViewController { |
| 526 | 541 | } |
| 527 | 542 | |
| 528 | 543 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| 529 | onOpen?(found[indexPath.row], query) | |
| 544 | let (notebook, pages) = found[indexPath.section] | |
| 545 | let page = pages[indexPath.row] | |
| 546 | (view.window?.windowScene?.delegate as? SceneDelegate)? | |
| 547 | .open(page.section, of: notebook, page: page.page, reveal: .text(query)) | |
| 530 | 548 | } |
| 531 | 549 | } |
apps/ios/Snowbound/Notebooks.swift+17| ... | ... | @@ -209,10 +209,27 @@ final class Notebook { |
| 209 | 209 | if let handle { sb_library_free(handle) } |
| 210 | 210 | handle = library |
| 211 | 211 | self.problem = library == nil ? (problem ?? "The notebook can’t be read.") : nil |
| 212 | follow() | |
| 212 | 213 | reload(done: done) |
| 213 | 214 | } |
| 214 | 215 | } |
| 215 | 216 | |
| 217 | /// Works offline or online, as Work Offline stands. | |
| 218 | func follow() { | |
| 219 | if let handle { sb_library_set_offline(handle, Sync.offline) } | |
| 220 | } | |
| 221 | ||
| 222 | /// Where the notebook is kept, as the sync sheet names it. | |
| 223 | var location: String { | |
| 224 | switch source { | |
| 225 | case .files: "In Files" | |
| 226 | case .documents: "On this \(UIDevice.current.model), in Snowbound’s folder" | |
| 227 | case .server(let server): | |
| 228 | "smb://\(server.host)/\([server.share, server.root].filter { !$0.isEmpty }.joined(separator: "/"))" | |
| 229 | case .path(let path): path | |
| 230 | } | |
| 231 | } | |
| 232 | ||
| 216 | 233 | /// Lists the sections again, off the main thread. |
| 217 | 234 | func reload(done: @escaping () -> Void) { |
| 218 | 235 | guard let handle else { |
apps/ios/Snowbound/PageViewController.swift+98-9| ... | ... | @@ -1,6 +1,14 @@ |
| 1 | 1 | import PhotosUI |
| 2 | 2 | import UIKit |
| 3 | 3 | |
| 4 | /// What a page shows selected as it opens. | |
| 5 | enum Reveal { | |
| 6 | /// The first match of a search. | |
| 7 | case text(String) | |
| 8 | /// A paragraph the Tags Summary lists, by id. | |
| 9 | case paragraph(String) | |
| 10 | } | |
| 11 | ||
| 4 | 12 | final class PageViewController: UIViewController, PHPickerViewControllerDelegate, |
| 5 | 13 | UIImagePickerControllerDelegate, UINavigationControllerDelegate |
| 6 | 14 | { |
| ... | ... | @@ -9,7 +17,7 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 9 | 17 | |
| 10 | 18 | private let section: Section |
| 11 | 19 | private let page: String |
| 12 | private let find: String? | |
| 20 | private let reveal: Reveal? | |
| 13 | 21 | private let titleFocus: Bool |
| 14 | 22 | private lazy var canvas = CanvasView(section: section, page: page) |
| 15 | 23 | private let bar = ConflictBar() |
| ... | ... | @@ -21,14 +29,18 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 21 | 29 | systemItem: .undo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: false) }) |
| 22 | 30 | private lazy var redo = UIBarButtonItem( |
| 23 | 31 | systemItem: .redo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: true) }) |
| 32 | private lazy var more = UIBarButtonItem( | |
| 33 | title: "Page", image: UIImage(systemName: "ellipsis.circle"), | |
| 34 | menu: UIMenu(children: [UIDeferredMenuElement.uncached { [weak self] done in done(self?.pageMenu() ?? []) }])) | |
| 24 | 35 | |
| 25 | init(section: Section, page: String, find: String? = nil, titleFocus: Bool = false) { | |
| 36 | init(section: Section, page: String, reveal: Reveal? = nil, titleFocus: Bool = false) { | |
| 26 | 37 | self.section = section |
| 27 | 38 | self.page = page |
| 28 | self.find = find | |
| 39 | self.reveal = reveal | |
| 29 | 40 | self.titleFocus = titleFocus |
| 30 | 41 | super.init(nibName: nil, bundle: nil) |
| 31 | 42 | navigationItem.largeTitleDisplayMode = .never |
| 43 | navigationItem.rightBarButtonItems = [more] | |
| 32 | 44 | showTitle(section.row(of: page)?.row.title ?? "") |
| 33 | 45 | } |
| 34 | 46 | |
| ... | ... | @@ -60,13 +72,21 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 60 | 72 | self, selector: #selector(sectionChanged), name: Section.changed, object: section) |
| 61 | 73 | NotificationCenter.default.addObserver(self, selector: #selector(leaving), name: Self.leaving, object: nil) |
| 62 | 74 | conflictChanged() |
| 75 | registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (controller: PageViewController, _) in | |
| 76 | controller.fitTitle() | |
| 77 | } | |
| 63 | 78 | } |
| 64 | 79 | |
| 65 | /// The page is on screen: a found word is selected, a new page's title takes the caret. | |
| 80 | /// The page is on screen: a found word or tagged paragraph is selected, a new page's | |
| 81 | /// title takes the caret. | |
| 66 | 82 | private func opened() { |
| 67 | if let find, canvas.find(find) { | |
| 68 | _ = canvas.becomeFirstResponder() | |
| 69 | } else if titleFocus, canvas.focusTitle() { | |
| 83 | let revealed = | |
| 84 | switch reveal { | |
| 85 | case .text(let query): canvas.find(query) | |
| 86 | case .paragraph(let id): canvas.selectParagraph(id) | |
| 87 | case nil: false | |
| 88 | } | |
| 89 | if revealed || titleFocus && canvas.focusTitle() { | |
| 70 | 90 | _ = canvas.becomeFirstResponder() |
| 71 | 91 | } |
| 72 | 92 | } |
| ... | ... | @@ -75,23 +95,92 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 75 | 95 | self.title = title.isEmpty ? "Untitled Page" : title |
| 76 | 96 | } |
| 77 | 97 | |
| 98 | /// A phone's bar has no room for the title beside the editing buttons, and the page | |
| 99 | /// shows it anyway, as Notes leaves it out. | |
| 100 | private func fitTitle() { | |
| 101 | let crowded = canvas.isFirstResponder && traitCollection.horizontalSizeClass == .compact | |
| 102 | navigationItem.titleView = crowded ? UIView() : nil | |
| 103 | } | |
| 104 | ||
| 78 | 105 | /// Notes' checkmark ends editing and puts the keyboard away; undo and redo sit beside it. |
| 79 | 106 | private func editingChanged() { |
| 80 | 107 | if !canvas.readOnly, let title = canvas.pageTitle { |
| 81 | 108 | showTitle(title) |
| 82 | 109 | section.retitle(page, title) |
| 83 | 110 | } |
| 111 | fitTitle() | |
| 84 | 112 | guard canvas.isFirstResponder else { |
| 85 | navigationItem.rightBarButtonItems = nil | |
| 113 | if navigationItem.rightBarButtonItems?.first !== more { navigationItem.rightBarButtonItems = [more] } | |
| 86 | 114 | return |
| 87 | 115 | } |
| 88 | 116 | undo.isEnabled = canvas.canUndo(redo: false) |
| 89 | 117 | redo.isEnabled = canvas.canUndo(redo: true) |
| 90 | 118 | if navigationItem.rightBarButtonItems?.first !== done { |
| 91 | navigationItem.rightBarButtonItems = [done, redo, undo] | |
| 119 | navigationItem.rightBarButtonItems = [done, more, redo, undo] | |
| 92 | 120 | } |
| 93 | 121 | } |
| 94 | 122 | |
| 123 | // MARK: Page menu | |
| 124 | ||
| 125 | /// The desktop's page and insert commands that suit a touch screen. | |
| 126 | private func pageMenu() -> [UIMenuElement] { | |
| 127 | let editable: UIMenuElement.Attributes = canvas.readOnly ? .disabled : [] | |
| 128 | let scene = view.window?.windowScene?.delegate as? SceneDelegate | |
| 129 | let insert = UIMenu( | |
| 130 | title: "Insert", image: UIImage(systemName: "plus.circle"), | |
| 131 | children: [ | |
| 132 | UIAction(title: "Photo Library", image: UIImage(systemName: "photo.on.rectangle"), attributes: editable) { | |
| 133 | [weak self] _ in self?.pickPicture(camera: false) | |
| 134 | }, | |
| 135 | UIAction( | |
| 136 | title: "Take Photo", image: UIImage(systemName: "camera"), | |
| 137 | attributes: UIImagePickerController.isSourceTypeAvailable(.camera) ? editable : .disabled | |
| 138 | ) { [weak self] _ in self?.pickPicture(camera: true) }, | |
| 139 | UIMenu(options: .displayInline, children: [ | |
| 140 | UIAction(title: "Date", image: UIImage(systemName: "calendar"), attributes: editable) { | |
| 141 | [weak self] _ in self?.canvas.insertDate(true, time: false) | |
| 142 | }, | |
| 143 | UIAction(title: "Time", image: UIImage(systemName: "clock"), attributes: editable) { | |
| 144 | [weak self] _ in self?.canvas.insertDate(false, time: true) | |
| 145 | }, | |
| 146 | UIAction(title: "Date & Time", image: UIImage(systemName: "calendar.badge.clock"), attributes: editable) { | |
| 147 | [weak self] _ in self?.canvas.insertDate(true, time: true) | |
| 148 | }, | |
| 149 | ]), | |
| 150 | UIAction( | |
| 151 | title: "Space", image: UIImage(systemName: "arrow.up.and.down.text.horizontal"), attributes: editable | |
| 152 | ) { [weak self] _ in self?.canvas.insertSpace() }, | |
| 153 | ]) | |
| 154 | return [ | |
| 155 | insert, | |
| 156 | UIAction(title: "Page Background…", image: UIImage(systemName: "paintpalette"), attributes: editable) { | |
| 157 | [weak self] _ in self?.showPaper() | |
| 158 | }, | |
| 159 | UIAction(title: "Tags Summary", image: UIImage(systemName: "tag")) { [weak self] _ in | |
| 160 | guard let self else { return } | |
| 161 | scene?.showTags(of: section.notebook, from: self) | |
| 162 | }, | |
| 163 | UIMenu(options: .displayInline, children: [ | |
| 164 | UIAction(title: "New Subpage", image: UIImage(systemName: "text.badge.plus")) { _ in | |
| 165 | scene?.newPage(subpage: true) | |
| 166 | }, | |
| 167 | UIAction(title: "Delete Page", image: UIImage(systemName: "trash"), attributes: .destructive) { | |
| 168 | [weak self] _ in | |
| 169 | guard let self else { return } | |
| 170 | section.delete(page) { _ in } | |
| 171 | }, | |
| 172 | ]), | |
| 173 | ] | |
| 174 | } | |
| 175 | ||
| 176 | private func showPaper() { | |
| 177 | let navigation = UINavigationController(rootViewController: PaperViewController(canvas: canvas)) | |
| 178 | navigation.sheetPresentationController?.detents = [.medium(), .large()] | |
| 179 | navigation.sheetPresentationController?.prefersGrabberVisible = true | |
| 180 | navigation.sheetPresentationController?.largestUndimmedDetentIdentifier = .medium | |
| 181 | present(navigation, animated: true) | |
| 182 | } | |
| 183 | ||
| 95 | 184 | @objc private func leaving() { canvas.commitComposition() } |
| 96 | 185 | |
| 97 | 186 | @objc private func sectionChanged(_ notification: Notification) { |
apps/ios/Snowbound/PaperViewController.swift created+183| ... | ... | @@ -0,0 +1,183 @@ |
| 1 | import UIKit | |
| 2 | ||
| 3 | /// The page's paper from `sb_view_paper`, with the choices OneNote offers. | |
| 4 | struct Paper: Decodable { | |
| 5 | struct Color: Decodable { | |
| 6 | let name: String | |
| 7 | let rgb: [UInt8] | |
| 8 | ||
| 9 | init(from decoder: Decoder) throws { | |
| 10 | var pair = try decoder.unkeyedContainer() | |
| 11 | name = try pair.decode(String.self) | |
| 12 | rgb = try pair.decode([UInt8].self) | |
| 13 | } | |
| 14 | } | |
| 15 | ||
| 16 | let color: [UInt8]? | |
| 17 | let ruled: Int? | |
| 18 | let colors: [Color] | |
| 19 | let rules: [String] | |
| 20 | let templates: [String] | |
| 21 | } | |
| 22 | ||
| 23 | extension UIColor { | |
| 24 | convenience init(rgb: [UInt8]) { | |
| 25 | self.init(red: CGFloat(rgb[0]) / 255, green: CGFloat(rgb[1]) / 255, blue: CGFloat(rgb[2]) / 255, alpha: 1) | |
| 26 | } | |
| 27 | ||
| 28 | var rgb: [UInt8] { | |
| 29 | var (red, green, blue, alpha): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) | |
| 30 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) | |
| 31 | return [red, green, blue].map { UInt8((min(1, max(0, $0)) * 255).rounded()) } | |
| 32 | } | |
| 33 | } | |
| 34 | ||
| 35 | /// OneNote's View, Page Color and Rule Lines, and a template's art behind the page, as a | |
| 36 | /// sheet over the page, each choice taking effect at once. | |
| 37 | final class PaperViewController: UITableViewController { | |
| 38 | private enum Part: Int, CaseIterable { case color, rules, background } | |
| 39 | ||
| 40 | private let canvas: CanvasView | |
| 41 | private var paper: Paper? | |
| 42 | private let swatches = UIStackView() | |
| 43 | private let well = UIColorWell() | |
| 44 | ||
| 45 | init(canvas: CanvasView) { | |
| 46 | self.canvas = canvas | |
| 47 | super.init(style: .insetGrouped) | |
| 48 | title = "Page Background" | |
| 49 | } | |
| 50 | ||
| 51 | required init?(coder: NSCoder) { fatalError() } | |
| 52 | ||
| 53 | override func viewDidLoad() { | |
| 54 | super.viewDidLoad() | |
| 55 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "row") | |
| 56 | navigationItem.rightBarButtonItem = UIBarButtonItem( | |
| 57 | systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) }) | |
| 58 | well.title = "Custom Color" | |
| 59 | well.supportsAlpha = false | |
| 60 | well.addAction(UIAction { [weak self] _ in | |
| 61 | guard let self, let color = well.selectedColor else { return } | |
| 62 | canvas.setPaper(color: color.rgb, ruled: paper?.ruled) | |
| 63 | reload() | |
| 64 | }, for: .valueChanged) | |
| 65 | reload() | |
| 66 | } | |
| 67 | ||
| 68 | private func reload() { | |
| 69 | paper = canvas.pagePaper | |
| 70 | showSwatches() | |
| 71 | tableView.reloadData() | |
| 72 | } | |
| 73 | ||
| 74 | private func showSwatches() { | |
| 75 | swatches.arrangedSubviews.forEach { $0.removeFromSuperview() } | |
| 76 | guard let paper else { return } | |
| 77 | let none = swatch(nil, name: "No Color", chosen: paper.color == nil) | |
| 78 | swatches.addArrangedSubview(none) | |
| 79 | for color in paper.colors { | |
| 80 | swatches.addArrangedSubview(swatch(color.rgb, name: color.name, chosen: paper.color == color.rgb)) | |
| 81 | } | |
| 82 | let custom = paper.color.flatMap { color in paper.colors.contains { $0.rgb == color } ? nil : color } | |
| 83 | well.selectedColor = custom.map(UIColor.init(rgb:)) | |
| 84 | swatches.addArrangedSubview(well) | |
| 85 | } | |
| 86 | ||
| 87 | private func swatch(_ rgb: [UInt8]?, name: String, chosen: Bool) -> UIButton { | |
| 88 | var configuration = UIButton.Configuration.plain() | |
| 89 | configuration.background.backgroundColor = rgb.map(UIColor.init(rgb:)) ?? .white | |
| 90 | configuration.background.cornerRadius = 16 | |
| 91 | configuration.background.strokeColor = chosen ? .tintColor : .separator | |
| 92 | configuration.background.strokeWidth = chosen ? 3 : 1 | |
| 93 | if rgb == nil { | |
| 94 | configuration.image = UIImage(systemName: "line.diagonal") | |
| 95 | configuration.baseForegroundColor = .systemRed | |
| 96 | } | |
| 97 | let button = UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in | |
| 98 | guard let self else { return } | |
| 99 | canvas.setPaper(color: rgb, ruled: paper?.ruled) | |
| 100 | reload() | |
| 101 | }) | |
| 102 | button.accessibilityLabel = name | |
| 103 | button.accessibilityTraits.insert(chosen ? .selected : []) | |
| 104 | button.widthAnchor.constraint(equalToConstant: 32).isActive = true | |
| 105 | button.heightAnchor.constraint(equalToConstant: 32).isActive = true | |
| 106 | return button | |
| 107 | } | |
| 108 | ||
| 109 | private let parts = Part.allCases | |
| 110 | ||
| 111 | override func numberOfSections(in tableView: UITableView) -> Int { parts.count } | |
| 112 | ||
| 113 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
| 114 | switch parts[section] { | |
| 115 | case .color: "Page Color" | |
| 116 | case .rules: "Rule Lines" | |
| 117 | case .background: "Background" | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
| 122 | guard let paper else { return 0 } | |
| 123 | return switch parts[section] { | |
| 124 | case .color: 1 | |
| 125 | case .rules: 1 + paper.rules.count | |
| 126 | case .background: 1 + paper.templates.count | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
| 131 | let cell = tableView.dequeueReusableCell(withIdentifier: "row", for: indexPath) | |
| 132 | cell.contentView.subviews.filter { $0 is UIScrollView }.forEach { $0.removeFromSuperview() } | |
| 133 | cell.accessoryType = .none | |
| 134 | cell.contentConfiguration = nil | |
| 135 | guard let paper else { return cell } | |
| 136 | switch parts[indexPath.section] { | |
| 137 | case .color: | |
| 138 | cell.selectionStyle = .none | |
| 139 | let scroll = UIScrollView() | |
| 140 | scroll.showsHorizontalScrollIndicator = false | |
| 141 | swatches.spacing = 12 | |
| 142 | swatches.alignment = .center | |
| 143 | swatches.translatesAutoresizingMaskIntoConstraints = false | |
| 144 | scroll.translatesAutoresizingMaskIntoConstraints = false | |
| 145 | scroll.addSubview(swatches) | |
| 146 | cell.contentView.addSubview(scroll) | |
| 147 | NSLayoutConstraint.activate([ | |
| 148 | scroll.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), | |
| 149 | scroll.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor), | |
| 150 | scroll.topAnchor.constraint(equalTo: cell.contentView.topAnchor), | |
| 151 | scroll.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), | |
| 152 | scroll.heightAnchor.constraint(equalToConstant: 56), | |
| 153 | swatches.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 16), | |
| 154 | swatches.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -16), | |
| 155 | swatches.centerYAnchor.constraint(equalTo: scroll.frameLayoutGuide.centerYAnchor), | |
| 156 | ]) | |
| 157 | case .rules: | |
| 158 | cell.selectionStyle = .default | |
| 159 | var content = cell.defaultContentConfiguration() | |
| 160 | content.text = indexPath.row == 0 ? "None" : paper.rules[indexPath.row - 1] | |
| 161 | cell.contentConfiguration = content | |
| 162 | let chosen = indexPath.row == 0 ? paper.ruled == nil : paper.ruled == indexPath.row - 1 | |
| 163 | cell.accessoryType = chosen ? .checkmark : .none | |
| 164 | case .background: | |
| 165 | cell.selectionStyle = .default | |
| 166 | var content = cell.defaultContentConfiguration() | |
| 167 | content.text = indexPath.row == 0 ? "None" : paper.templates[indexPath.row - 1] | |
| 168 | cell.contentConfiguration = content | |
| 169 | } | |
| 170 | return cell | |
| 171 | } | |
| 172 | ||
| 173 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
| 174 | tableView.deselectRow(at: indexPath, animated: true) | |
| 175 | guard let paper else { return } | |
| 176 | switch parts[indexPath.section] { | |
| 177 | case .color: return | |
| 178 | case .rules: canvas.setPaper(color: paper.color, ruled: indexPath.row == 0 ? nil : indexPath.row - 1) | |
| 179 | case .background: canvas.setArt(indexPath.row == 0 ? nil : paper.templates[indexPath.row - 1]) | |
| 180 | } | |
| 181 | reload() | |
| 182 | } | |
| 183 | } |
apps/ios/Snowbound/Script.swift+4-1| ... | ... | @@ -10,7 +10,7 @@ extension CanvasView { |
| 10 | 10 | /// `tap` and `doubletap` take points from the view's corner, `select` text offsets, |
| 11 | 11 | /// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word` |
| 12 | 12 | /// selects a match, `title` edits the title, `picture` inserts a drawn picture, `selectall` |
| 13 | /// is the Select All command; `done` | |
| 13 | /// is the Select All command; `rotate:landscape` or `rotate:portrait` turns the device; `done` | |
| 14 | 14 | /// ends editing, `tree` saves the view hierarchy to |
| 15 | 15 | /// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no |
| 16 | 16 | /// screenshot command. |
| ... | ... | @@ -54,6 +54,9 @@ extension CanvasView { |
| 54 | 54 | case "doubletap": |
| 55 | 55 | let values = argument.split(separator: ",").compactMap { Double($0) } |
| 56 | 56 | zoom(at: CGPoint(x: values[0] + contentOffset.x, y: values[1] + contentOffset.y)) |
| 57 | case "rotate": | |
| 58 | let orientation: UIInterfaceOrientationMask = argument == "portrait" ? .portrait : .landscapeRight | |
| 59 | window?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation)) | |
| 57 | 60 | case "done": _ = resignFirstResponder() |
| 58 | 61 | case "selectall": selectAll(nil) |
| 59 | 62 | case "format": apply(UInt8(argument) ?? 0) |
apps/ios/Snowbound/Sync.swift created+260| ... | ... | @@ -0,0 +1,260 @@ |
| 1 | import UIKit | |
| 2 | ||
| 3 | /// A section's sync status from `sb_library_sync_status`. | |
| 4 | struct SectionSync: Decodable { | |
| 5 | enum State: UInt8, Decodable, Comparable { | |
| 6 | case upToDate, syncing, inUse, notConnected, readOnly, failed | |
| 7 | ||
| 8 | static func < (a: State, b: State) -> Bool { a.rawValue < b.rawValue } | |
| 9 | } | |
| 10 | ||
| 11 | let path: String | |
| 12 | let state: State | |
| 13 | /// Seconds since 1970 when the section file was last reached. | |
| 14 | let synced: Double? | |
| 15 | let queued: Int | |
| 16 | let error: String? | |
| 17 | } | |
| 18 | ||
| 19 | /// OneNote's Work Offline and Sync Now over every open notebook, and their status. | |
| 20 | enum Sync { | |
| 21 | /// Posted on the main thread when a notebook's closed sections report. | |
| 22 | static let changed = Notification.Name("SyncChanged") | |
| 23 | private static let key = "offline" | |
| 24 | ||
| 25 | static var offline: Bool { | |
| 26 | get { UserDefaults.standard.bool(forKey: key) } | |
| 27 | set { | |
| 28 | UserDefaults.standard.set(newValue, forKey: key) | |
| 29 | for notebook in Notebooks.all { notebook.follow() } | |
| 30 | NotificationCenter.default.post(name: changed, object: nil) | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | static func now() { | |
| 35 | for notebook in Notebooks.all { | |
| 36 | if let handle = notebook.handle { sb_library_sync_now(handle) } | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | /// Every open notebook's sections with their status, read off the main thread. | |
| 41 | static func status(done: @escaping ([(Notebook, [SectionSync])]) -> Void) { | |
| 42 | let notebooks = Notebooks.all.compactMap { notebook in notebook.handle.map { (notebook, Int(bitPattern: $0)) } } | |
| 43 | background({ | |
| 44 | notebooks.map { notebook, pointer in | |
| 45 | (notebook, decode([SectionSync].self, sb_library_sync_status(OpaquePointer(bitPattern: pointer))) ?? []) | |
| 46 | } | |
| 47 | }, done: done) | |
| 48 | } | |
| 49 | ||
| 50 | /// What `state` comes to for the reader, as the desktop names it. | |
| 51 | static func label(_ state: SectionSync.State) -> String { | |
| 52 | if offline { return "Working offline" } | |
| 53 | return switch state { | |
| 54 | case .upToDate: "Up to date" | |
| 55 | case .syncing: "Syncing…" | |
| 56 | case .inUse: "Section in use" | |
| 57 | case .notConnected: "Not connected" | |
| 58 | case .readOnly, .failed: "Unable to sync" | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | static func symbol(_ state: SectionSync.State) -> String { | |
| 63 | if offline { return "icloud.slash" } | |
| 64 | return switch state { | |
| 65 | case .upToDate: "checkmark.circle" | |
| 66 | case .syncing, .inUse: "arrow.triangle.2.circlepath" | |
| 67 | case .notConnected: "wifi.slash" | |
| 68 | case .readOnly, .failed: "exclamationmark.triangle" | |
| 69 | } | |
| 70 | } | |
| 71 | ||
| 72 | /// What the reader can do about `state`. | |
| 73 | static func advice(_ state: SectionSync.State) -> String? { | |
| 74 | let device = UIDevice.current.model | |
| 75 | if offline { return "Changes stay on this \(device) until you sync." } | |
| 76 | return switch state { | |
| 77 | case .readOnly: "You can’t change this notebook where it’s stored. Changes stay on this \(device)." | |
| 78 | case .inUse: "Someone else is saving this section. Sync continues when they finish." | |
| 79 | case .notConnected: "Changes stay on this \(device) and sync when the notebook is back." | |
| 80 | default: nil | |
| 81 | } | |
| 82 | } | |
| 83 | ||
| 84 | static func changes(_ count: Int) -> String { count == 1 ? "1 change" : "\(count) changes" } | |
| 85 | ||
| 86 | /// When a section was last reached: the time, and the date too before today. | |
| 87 | static func when(_ seconds: Double) -> String { | |
| 88 | let date = Date(timeIntervalSince1970: seconds) | |
| 89 | return Calendar.current.isDateInToday(date) | |
| 90 | ? DateFormatter.localizedString(from: date, dateStyle: .none, timeStyle: .short) | |
| 91 | : DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .short) | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | /// The sync status in a list's toolbar, as Mail shows when it last checked; opens the sync | |
| 96 | /// sheet. | |
| 97 | final class SyncIndicator: UIButton { | |
| 98 | init() { | |
| 99 | var configuration = UIButton.Configuration.plain() | |
| 100 | configuration.imagePadding = 4 | |
| 101 | configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .caption1) | |
| 102 | configuration.baseForegroundColor = .secondaryLabel | |
| 103 | configuration.titleLineBreakMode = .byTruncatingTail | |
| 104 | configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { | |
| 105 | var attributes = $0 | |
| 106 | attributes.font = .preferredFont(forTextStyle: .caption1) | |
| 107 | return attributes | |
| 108 | } | |
| 109 | super.init(frame: .zero) | |
| 110 | self.configuration = configuration | |
| 111 | addAction(UIAction { [weak self] _ in self?.present() }, for: .primaryActionTriggered) | |
| 112 | for name in [Sync.changed, Section.changed] { | |
| 113 | NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil) | |
| 114 | } | |
| 115 | refresh() | |
| 116 | } | |
| 117 | ||
| 118 | required init?(coder: NSCoder) { fatalError() } | |
| 119 | ||
| 120 | @objc private func refresh() { | |
| 121 | Sync.status { [weak self] notebooks in | |
| 122 | let sections = notebooks.flatMap(\.1) | |
| 123 | guard let self, let worst = sections.map(\.state).max() else { | |
| 124 | self?.isHidden = true | |
| 125 | return | |
| 126 | } | |
| 127 | isHidden = false | |
| 128 | let queued = sections.map(\.queued).reduce(0, +) | |
| 129 | let label = Sync.label(worst) | |
| 130 | configuration?.title = queued > 0 && worst != .syncing ? "\(label), \(Sync.changes(queued))" : label | |
| 131 | configuration?.image = UIImage(systemName: Sync.symbol(worst)) | |
| 132 | configuration?.baseForegroundColor = worst >= .notConnected && !Sync.offline ? .systemOrange : .secondaryLabel | |
| 133 | accessibilityLabel = "Sync status: \(configuration?.title ?? label)" | |
| 134 | sizeToFit() | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | private func present() { | |
| 139 | guard let controller = window?.rootViewController else { return } | |
| 140 | let navigation = UINavigationController(rootViewController: SyncViewController()) | |
| 141 | navigation.sheetPresentationController?.detents = [.medium(), .large()] | |
| 142 | navigation.sheetPresentationController?.prefersGrabberVisible = true | |
| 143 | (controller.presentedViewController ?? controller).present(navigation, animated: true) | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | /// OneNote's Notebook Sync Status: Work Offline, Sync Now, and each notebook's sections. | |
| 148 | final class SyncViewController: UITableViewController { | |
| 149 | private var notebooks: [(Notebook, [SectionSync])] = [] | |
| 150 | private var timer: Timer? | |
| 151 | private let offline = UISwitch() | |
| 152 | ||
| 153 | init() { | |
| 154 | super.init(style: .insetGrouped) | |
| 155 | title = "Sync Status" | |
| 156 | } | |
| 157 | ||
| 158 | required init?(coder: NSCoder) { fatalError() } | |
| 159 | ||
| 160 | override func viewDidLoad() { | |
| 161 | super.viewDidLoad() | |
| 162 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "row") | |
| 163 | navigationItem.rightBarButtonItem = UIBarButtonItem( | |
| 164 | systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) }) | |
| 165 | offline.addAction(UIAction { [weak self] _ in | |
| 166 | Sync.offline = self?.offline.isOn ?? false | |
| 167 | }, for: .valueChanged) | |
| 168 | for name in [Sync.changed, Section.changed] { | |
| 169 | NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil) | |
| 170 | } | |
| 171 | } | |
| 172 | ||
| 173 | override func viewWillAppear(_ animated: Bool) { | |
| 174 | super.viewWillAppear(animated) | |
| 175 | refresh() | |
| 176 | // The last sync time follows each round, which reports only when something changes. | |
| 177 | timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in self?.refresh() } | |
| 178 | } | |
| 179 | ||
| 180 | override func viewDidDisappear(_ animated: Bool) { | |
| 181 | super.viewDidDisappear(animated) | |
| 182 | timer?.invalidate() | |
| 183 | } | |
| 184 | ||
| 185 | @objc private func refresh() { | |
| 186 | Sync.status { [weak self] notebooks in | |
| 187 | self?.notebooks = notebooks.filter { !$0.1.isEmpty } | |
| 188 | self?.tableView.reloadData() | |
| 189 | } | |
| 190 | } | |
| 191 | ||
| 192 | override func numberOfSections(in tableView: UITableView) -> Int { 1 + notebooks.count } | |
| 193 | ||
| 194 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
| 195 | section == 0 ? 2 : notebooks[section - 1].1.count | |
| 196 | } | |
| 197 | ||
| 198 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
| 199 | section == 0 ? nil : notebooks[section - 1].0.name | |
| 200 | } | |
| 201 | ||
| 202 | override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { | |
| 203 | guard section > 0 else { return Sync.offline ? Sync.advice(.upToDate) : nil } | |
| 204 | let (notebook, sections) = notebooks[section - 1] | |
| 205 | let worst = sections.map(\.state).max() ?? .upToDate | |
| 206 | let synced = sections.map(\.synced) | |
| 207 | let oldest = synced.contains(where: { $0 == nil }) ? nil : synced.compactMap { $0 }.min() | |
| 208 | return [ | |
| 209 | notebook.location, | |
| 210 | oldest.map { "Last sync \(Sync.when($0))" }, | |
| 211 | Sync.offline ? nil : Sync.advice(worst), | |
| 212 | ].compactMap { $0 }.joined(separator: "\n") | |
| 213 | } | |
| 214 | ||
| 215 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
| 216 | let cell = tableView.dequeueReusableCell(withIdentifier: "row", for: indexPath) | |
| 217 | cell.accessoryView = nil | |
| 218 | cell.selectionStyle = .none | |
| 219 | if indexPath.section == 0 { | |
| 220 | var content = UIListContentConfiguration.cell() | |
| 221 | if indexPath.row == 0 { | |
| 222 | content.text = "Work Offline" | |
| 223 | offline.isOn = Sync.offline | |
| 224 | cell.accessoryView = offline | |
| 225 | } else { | |
| 226 | content.text = "Sync Now" | |
| 227 | content.textProperties.color = .tintColor | |
| 228 | cell.selectionStyle = .default | |
| 229 | } | |
| 230 | cell.contentConfiguration = content | |
| 231 | return cell | |
| 232 | } | |
| 233 | let (notebook, sections) = notebooks[indexPath.section - 1] | |
| 234 | let sync = sections[indexPath.row] | |
| 235 | var content = UIListContentConfiguration.valueCell() | |
| 236 | let tab = notebook.tabs.first { $0.path == sync.path } | |
| 237 | content.text = tab?.name ?? URL(fileURLWithPath: sync.path).deletingPathExtension().lastPathComponent | |
| 238 | content.image = UIImage(systemName: "rectangle.portrait.fill") | |
| 239 | content.imageProperties.tintColor = tab?.uiColor ?? .systemGray | |
| 240 | let label = Sync.label(sync.state) | |
| 241 | content.secondaryText = sync.queued > 0 ? "\(label), \(Sync.changes(sync.queued))" : label | |
| 242 | if let error = sync.error { | |
| 243 | content = UIListContentConfiguration.subtitleCell() | |
| 244 | content.text = tab?.name ?? sync.path | |
| 245 | content.image = UIImage(systemName: "rectangle.portrait.fill") | |
| 246 | content.imageProperties.tintColor = tab?.uiColor ?? .systemGray | |
| 247 | content.secondaryText = "\(label): \(error)" | |
| 248 | content.secondaryTextProperties.color = .secondaryLabel | |
| 249 | } | |
| 250 | cell.contentConfiguration = content | |
| 251 | return cell | |
| 252 | } | |
| 253 | ||
| 254 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
| 255 | tableView.deselectRow(at: indexPath, animated: true) | |
| 256 | guard indexPath == IndexPath(row: 1, section: 0) else { return } | |
| 257 | Sync.now() | |
| 258 | refresh() | |
| 259 | } | |
| 260 | } |
apps/ios/Snowbound/Tags.swift created+193| ... | ... | @@ -0,0 +1,193 @@ |
| 1 | import UIKit | |
| 2 | ||
| 3 | /// OneNote's tags in `sb_view_apply`'s order from 16, drawn with the page's artwork. | |
| 4 | enum Tags { | |
| 5 | struct Tag { | |
| 6 | let name: String | |
| 7 | /// The symbol `sb_tag_icon` draws; 0 for a highlighting tag. | |
| 8 | let shape: UInt16 | |
| 9 | let highlight: UIColor? | |
| 10 | } | |
| 11 | ||
| 12 | static let all: [Tag] = { | |
| 13 | guard let text = take(sb_tags()), | |
| 14 | let rows = try? JSONSerialization.jsonObject(with: Data(text.utf8)) as? [[Any]] | |
| 15 | else { return [] } | |
| 16 | return rows.compactMap { row in | |
| 17 | guard row.count == 3, let name = row[0] as? String, let shape = row[1] as? NSNumber else { return nil } | |
| 18 | let highlight = (row[2] as? [NSNumber]).map { UIColor(rgb: $0.map(\.uint8Value)) } | |
| 19 | return Tag(name: name, shape: shape.uint16Value, highlight: highlight) | |
| 20 | } | |
| 21 | }() | |
| 22 | ||
| 23 | /// The first nine, which ⌘1 to ⌘9 apply, as Ctrl+1 to Ctrl+9 do in OneNote. | |
| 24 | static let keyed = 9 | |
| 25 | ||
| 26 | private static var icons: [Int: UIImage] = [:] | |
| 27 | ||
| 28 | /// Tag symbol `shape` as OneNote draws it, a check box `checked`; a highlighter in | |
| 29 | /// `highlight` for a highlighting tag. | |
| 30 | static func icon(_ shape: UInt16, checked: Bool = false, highlight: UIColor? = nil) -> UIImage? { | |
| 31 | guard shape != 0 else { | |
| 32 | // Highlighted lines, as the desktop draws these tags. | |
| 33 | let fill = highlight ?? .systemYellow | |
| 34 | return UIGraphicsImageRenderer(size: CGSize(width: 18, height: 18)).image { context in | |
| 35 | let card = UIBezierPath(roundedRect: CGRect(x: 1.5, y: 3.5, width: 15, height: 11), cornerRadius: 1.5) | |
| 36 | fill.setFill() | |
| 37 | card.fill() | |
| 38 | UIColor(white: 0, alpha: 0.35).setStroke() | |
| 39 | card.stroke() | |
| 40 | UIColor(white: 0.25, alpha: 1).setFill() | |
| 41 | context.fill(CGRect(x: 4, y: 6.5, width: 10, height: 1.5)) | |
| 42 | context.fill(CGRect(x: 4, y: 10, width: 7, height: 1.5)) | |
| 43 | } | |
| 44 | } | |
| 45 | let key = Int(shape) << 1 | (checked ? 1 : 0) | |
| 46 | if let icon = icons[key] { return icon } | |
| 47 | let points: CGFloat = 18 | |
| 48 | let scale = UIScreen.main.scale | |
| 49 | let pixels = Int(points * scale) | |
| 50 | var rgba = [UInt8](repeating: 0, count: pixels * pixels * 4) | |
| 51 | guard sb_tag_icon(shape, checked, UInt32(pixels), &rgba), | |
| 52 | let provider = CGDataProvider(data: Data(rgba) as CFData), | |
| 53 | let image = CGImage( | |
| 54 | width: pixels, height: pixels, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: pixels * 4, | |
| 55 | space: CGColorSpace(name: CGColorSpace.sRGB)!, | |
| 56 | bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), | |
| 57 | provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) | |
| 58 | else { return UIImage(systemName: "tag") } | |
| 59 | let icon = UIImage(cgImage: image, scale: scale, orientation: .up).withRenderingMode(.alwaysOriginal) | |
| 60 | icons[key] = icon | |
| 61 | return icon | |
| 62 | } | |
| 63 | ||
| 64 | /// Every tag as a menu, those the selection has checked, then Remove Tag; `apply` takes | |
| 65 | /// the `sb_view_apply` command. | |
| 66 | static func menu(bits: UInt64, apply: @escaping (UInt8) -> Void) -> [UIMenuElement] { | |
| 67 | let actions = all.enumerated().map { index, tag in | |
| 68 | UIAction( | |
| 69 | title: tag.name, image: icon(tag.shape, highlight: tag.highlight), | |
| 70 | state: bits & (1 << UInt64(16 + index)) != 0 ? .on : .off | |
| 71 | ) { _ in apply(16 + UInt8(index)) } | |
| 72 | } | |
| 73 | let tagged = bits >> 16 != 0 | |
| 74 | return [ | |
| 75 | UIMenu(options: .displayInline, children: Array(actions.prefix(keyed))), | |
| 76 | UIMenu(title: "More Tags", image: UIImage(systemName: "tag"), children: Array(actions.dropFirst(keyed))), | |
| 77 | UIAction( | |
| 78 | title: "Remove Tag", image: UIImage(systemName: "tag.slash"), | |
| 79 | attributes: tagged ? [] : .disabled | |
| 80 | ) { _ in apply(8) }, | |
| 81 | ] | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | /// A tagged paragraph from `sb_library_tagged`. | |
| 86 | struct Tagged: Decodable { | |
| 87 | let section: String | |
| 88 | let page: String | |
| 89 | let title: String | |
| 90 | let paragraph: String | |
| 91 | let name: String | |
| 92 | let shape: UInt16 | |
| 93 | let checked: Bool | |
| 94 | let text: String | |
| 95 | } | |
| 96 | ||
| 97 | /// OneNote's Tags Summary: a notebook's tagged paragraphs by tag, each opening its page with | |
| 98 | /// the paragraph selected. | |
| 99 | final class TagsViewController: UITableViewController { | |
| 100 | private let notebook: Notebook | |
| 101 | private var groups: [(name: String, items: [Tagged])] = [] | |
| 102 | private var unchecked = false | |
| 103 | private var all: [Tagged] = [] | |
| 104 | /// Opens a page of the notebook: section path, page and paragraph. | |
| 105 | var onOpen: ((Notebook, String, String, String) -> Void)? | |
| 106 | ||
| 107 | init(notebook: Notebook) { | |
| 108 | self.notebook = notebook | |
| 109 | super.init(style: .insetGrouped) | |
| 110 | title = "Tags Summary" | |
| 111 | } | |
| 112 | ||
| 113 | required init?(coder: NSCoder) { fatalError() } | |
| 114 | ||
| 115 | override func viewDidLoad() { | |
| 116 | super.viewDidLoad() | |
| 117 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tagged") | |
| 118 | navigationItem.rightBarButtonItem = UIBarButtonItem( | |
| 119 | systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) }) | |
| 120 | navigationItem.leftBarButtonItem = UIBarButtonItem( | |
| 121 | title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle"), | |
| 122 | menu: UIMenu(children: [ | |
| 123 | UIDeferredMenuElement.uncached { [weak self] done in | |
| 124 | done([ | |
| 125 | UIAction(title: "Unchecked Only", state: self?.unchecked == true ? .on : .off) { _ in | |
| 126 | self?.unchecked.toggle() | |
| 127 | self?.show() | |
| 128 | } | |
| 129 | ]) | |
| 130 | } | |
| 131 | ])) | |
| 132 | var loading = UIContentUnavailableConfiguration.loading() | |
| 133 | loading.text = "Finding Tags…" | |
| 134 | contentUnavailableConfiguration = loading | |
| 135 | guard let library = notebook.handle else { return } | |
| 136 | let open = Section.all.first { $0.notebook === notebook } | |
| 137 | let (pointer, section, path) = (Int(bitPattern: library), open.map { Int(bitPattern: $0.handle) } ?? 0, open?.tab.path) | |
| 138 | background({ | |
| 139 | decode([Tagged].self, sb_library_tagged(OpaquePointer(bitPattern: pointer), OpaquePointer(bitPattern: section), path)) | |
| 140 | ?? [] | |
| 141 | }) { [weak self] tagged in | |
| 142 | self?.all = tagged | |
| 143 | self?.show() | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | private func show() { | |
| 148 | var order: [String] = [] | |
| 149 | var byName: [String: [Tagged]] = [:] | |
| 150 | for tagged in all where !unchecked || !tagged.checked { | |
| 151 | if byName[tagged.name] == nil { order.append(tagged.name) } | |
| 152 | byName[tagged.name, default: []].append(tagged) | |
| 153 | } | |
| 154 | groups = order.map { ($0, byName[$0] ?? []) } | |
| 155 | tableView.reloadData() | |
| 156 | var empty = UIContentUnavailableConfiguration.empty() | |
| 157 | empty.image = UIImage(systemName: "tag") | |
| 158 | empty.text = unchecked ? "No Unchecked Tags" : "No Tags" | |
| 159 | empty.secondaryText = unchecked ? nil : "Tag a paragraph from the format bar or its menu." | |
| 160 | contentUnavailableConfiguration = groups.isEmpty ? empty : nil | |
| 161 | } | |
| 162 | ||
| 163 | override func numberOfSections(in tableView: UITableView) -> Int { groups.count } | |
| 164 | ||
| 165 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { | |
| 166 | groups[section].name | |
| 167 | } | |
| 168 | ||
| 169 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
| 170 | groups[section].items.count | |
| 171 | } | |
| 172 | ||
| 173 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
| 174 | let tagged = groups[indexPath.section].items[indexPath.row] | |
| 175 | let cell = tableView.dequeueReusableCell(withIdentifier: "tagged", for: indexPath) | |
| 176 | var content = UIListContentConfiguration.subtitleCell() | |
| 177 | content.image = Tags.icon(tagged.shape, checked: tagged.checked) | |
| 178 | content.text = tagged.text.isEmpty ? " " : tagged.text | |
| 179 | content.textProperties.numberOfLines = 3 | |
| 180 | let section = notebook.tabs.first { $0.path == tagged.section }?.name | |
| 181 | content.secondaryText = [tagged.title.isEmpty ? "Untitled Page" : tagged.title, section] | |
| 182 | .compactMap { $0 }.joined(separator: " · ") | |
| 183 | content.secondaryTextProperties.color = .secondaryLabel | |
| 184 | cell.contentConfiguration = content | |
| 185 | return cell | |
| 186 | } | |
| 187 | ||
| 188 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
| 189 | let tagged = groups[indexPath.section].items[indexPath.row] | |
| 190 | dismiss(animated: true) | |
| 191 | onOpen?(notebook, tagged.section, tagged.page, tagged.paragraph) | |
| 192 | } | |
| 193 | } |
arc/canvas.md+20-1| ... | ... | @@ -41,7 +41,10 @@ the size of the edit, not the size of the page. |
| 41 | 41 | |
| 42 | 42 | Undo lives in the editor, not in storage. Each history entry keeps the inverse |
| 43 | 43 | of the change it made. Undoing applies that inverse and emits its ops as a new |
| 44 | edit, so the file only ever moves forward, like OneNote's. Undoing a deletion | |
| 44 | edit, so the file only ever moves forward, like OneNote's. Undo steps are | |
| 45 | OneNote 2010's too: typing and backspaces at one caret are a single step until | |
| 46 | the caret moves or another kind of edit comes between, though each keystroke's | |
| 47 | ops still reach storage as it is typed. Undoing a deletion | |
| 45 | 48 | brings back the original identities, so internal links to those paragraphs |
| 46 | 49 | survive an undo. |
| 47 | 50 | |
| ... | ... | @@ -87,6 +90,22 @@ OneNote links it when it opens a page, without being stored as one. Ink draws st |
| 87 | 90 | coordinates. Page templates' background art is recreated as vector art and |
| 88 | 91 | recognised by the stored picture's hash. OneNote's bitmaps aren't shipped. |
| 89 | 92 | |
| 93 | ## Attached files | |
| 94 | ||
| 95 | A file sits in an outline's flow as OneNote draws it: its icon over its name, | |
| 96 | extension hidden, in a 54-point column. Attaching splits the caret's paragraph | |
| 97 | around the file, and the text after the caret follows it with the caret, as | |
| 98 | OneNote 2010 attaches and drops files. Every new file stores the icon OneNote | |
| 99 | stores beside it, because OneNote draws a broken picture for a file without | |
| 100 | one: the host's system icon where it has one, the canvas's blank page | |
| 101 | otherwise. A double click or the context menu's Open asks the host to open a | |
| 102 | copy; Save As writes the bytes where the user chooses. | |
| 103 | ||
| 104 | Attached or dropped where a click on blank page left the caret, the file goes | |
| 105 | on the page itself, as OneNote 2010 places it: the same column at the caret's | |
| 106 | grid point, outside any outline. It selects, drags on the grid, deletes and | |
| 107 | moves with Insert Space as a picture does, and opens and saves as above. | |
| 108 | ||
| 90 | 109 | ## Content the editor doesn't understand |
| 91 | 110 | |
| 92 | 111 | Nothing is lost for being unfamiliar. A paragraph the canvas can't draw |
arc/file-format.md+8-3| ... | ... | @@ -54,9 +54,14 @@ the file grows): |
| 54 | 54 | file mean nothing until the header's transaction count says a transaction |
| 55 | 55 | holding them has committed. |
| 56 | 56 | |
| 57 | External payloads (large pictures, attachments, recordings) can live beside | |
| 58 | the section as `.onebin` files in a `_onefiles` folder. The section refers to | |
| 59 | them by name. | |
| 57 | The format lets payloads (pictures, attachments, recordings) live beside the | |
| 58 | section as `.onebin` files in a `_onefiles` folder, referred to by name. OneNote | |
| 59 | 2010 never writes them: every attachment it stores, 300 MiB ones included and | |
| 60 | on a share too, goes into the section's own file data store, with a 32-pixel | |
| 61 | PNG of the file's icon beside it. Snowbound writes the same. OneNote also | |
| 62 | stores identical bytes once per section, so two attachments of one file (or | |
| 63 | one icon) share a payload; Snowbound stores each anew, which OneNote reads the | |
| 64 | same. | |
| 60 | 65 | |
| 61 | 66 | ## What "append one revision" means |
| 62 | 67 |
arc/sync.md+7| ... | ... | @@ -58,6 +58,13 @@ replayed and the open batch applied, rebuilt on open. The database runs in WAL |
| 58 | 58 | mode with full synchronous commits, and every setting is read back to check it |
| 59 | 59 | took. |
| 60 | 60 | |
| 61 | An attached file is a payload like a picture: its bytes ride in the edit that | |
| 62 | inserts it and publish inside that edit's revision. No other file is written, | |
| 63 | so nothing needs to reach the share before the revision that names it. That | |
| 64 | ordering would matter only for `_onefiles` payloads, which OneNote 2010 never | |
| 65 | writes ([file format](file-format.md)). A large file makes a publication as | |
| 66 | large as itself. | |
| 67 | ||
| 61 | 68 | A keystroke that publishes immediately costs three commits: the edit, the seal |
| 62 | 69 | (which also records the publication attempt), and the receipt. Each one is |
| 63 | 70 | ordered against something outside the cache. The edit must be durable before |
arc/ui.md+6-4| ... | ... | @@ -116,7 +116,8 @@ rounded or gradient rectangles, pen strokes and raster images. |
| 116 | 116 | - **Glyphs** are rasterized with Swash into an atlas, at quarter-pixel phases, |
| 117 | 117 | so text placed at fractional positions stays crisp and doesn't shimmer as it |
| 118 | 118 | moves. Icons and paths rasterize once per size and phase into the same |
| 119 | atlas. | |
| 119 | atlas. A rounded box's soft shadow is worked out in the shader instead, so | |
| 120 | popups of any size, widening or not, cost the atlas nothing. | |
| 120 | 121 | - **The renderer never measures text.** Text reaches it through a trait that |
| 121 | 122 | visits positioned glyph runs. Line metrics stay with whoever laid the text |
| 122 | 123 | out, which is how the page keeps its Windows metrics while the chrome uses |
| ... | ... | @@ -128,9 +129,10 @@ rounded or gradient rectangles, pen strokes and raster images. |
| 128 | 129 | or within shows through a row. A popup's contents clip to its rounded |
| 129 | 130 | outline, and one opening over a combo lays its rows out at their final |
| 130 | 131 | width while only the outline widens. |
| 131 | - **Caches are bounded.** The glyph atlas and image cache have fixed budgets. | |
| 132 | Eviction keeps everything the current frame needs and rebuilds after | |
| 133 | pressure. Images are filtered in linear light with premultiplied alpha. | |
| 132 | - **Caches are bounded.** The image cache has a fixed budget. A full glyph | |
| 133 | atlas is cleared and rebuilt with only what the current frame needs; where | |
| 134 | one frame alone needs more than half of it, it doubles, up to what the GPU | |
| 135 | allows. Images are filtered in linear light with premultiplied alpha. | |
| 134 | 136 | |
| 135 | 137 | GPU readback tests pin the rasterization down: successive quarter-pixel |
| 136 | 138 | translations have to move the ink's centroid in quarter-pixel steps, and |
corpus/attachment-edit/README.md+2-1| ... | ... | @@ -10,7 +10,8 @@ section's file-data store list, an embedded-file object declaring it by |
| 10 | 10 | identity and extension, and an attachment object with the file name, the |
| 11 | 11 | 24 pt icon size and the flags every native attachment carries. `icon/` also |
| 12 | 12 | embeds the native fixture's 724-byte icon PNG as the preview picture; |
| 13 | `plain/` omits it, which OneNote accepts (it renders its own icon). | |
| 13 | `plain/` omits it, which OneNote opens but draws as a broken picture | |
| 14 | (`corpus/attachment-insert/without-icon.png`), so the editor always stores one. | |
| 14 | 15 | OneNote's integrity check refuses an attachment object without |
| 15 | 16 | `0x1c001d61` (twenty bytes: 16, 1, zeros), so the writer always stores it. |
| 16 | 17 |
corpus/attachment-floating/README.md created+38| ... | ... | @@ -0,0 +1,38 @@ |
| 1 | # Files on the page | |
| 2 | ||
| 3 | `native/files.one` is OneNote 2010's own section, made in a lab clone on | |
| 4 | 2026-09-29. Its second page holds two copies of `float.txt` (38 bytes) outside | |
| 5 | any outline: one inserted through COM `UpdatePageContent` as a page-level | |
| 6 | `InsertedFile` at (216, 180) and then dragged to (288, 140.4), the other | |
| 7 | attached with Insert > Attach File after a click on blank page, at (342, 284.4). | |
| 8 | ||
| 9 | `candidate/` is `crates/canvas/tests/attachment_insert.rs`'s second section: | |
| 10 | the editor places the caret on blank page at (342, 284.4) and attaches | |
| 11 | `float 🦀.txt` (37 bytes, with the icon OneNote stored for a text file), then | |
| 12 | at (126, 410.4) attaches `large.bin` (2 MiB of noise, with the canvas's | |
| 13 | blank-page icon) and drags it to (288, 140.4). Each step is its own edit of the | |
| 14 | ops the editor recorded, sealed into one appended revision. `cold/` is a fresh | |
| 15 | OneNote 2010 read: both files are page-level `InsertedFile`s at those | |
| 16 | positions, and the bytes OneNote materializes to open them | |
| 17 | (`read/*.attachment`) hash as written. `tools/test_attachment_floating.py` | |
| 18 | checks the capture without a VM. Regenerate with `CANVAS_FLOATING_EXPORT` set to | |
| 19 | a directory while running the test, then `tools/native_runner.py DIR COLD | |
| 20 | --expected-pages 1 --collect-notebook --screenshots`. | |
| 21 | ||
| 22 | ## What OneNote 2010 does | |
| 23 | ||
| 24 | - **Placement.** Attached or dropped on blank page, the file becomes a child of | |
| 25 | the page at the point a click there puts the caret: on the 18 pt grid from | |
| 26 | the margin origin, 7 px above the pointer. It draws as a file in an outline | |
| 27 | does, its 32-pixel icon at 24 points centred in a 54-point column over its | |
| 28 | name without the extension, and selects as a shaded column framed in dashes, | |
| 29 | without handles. | |
| 30 | - **Storage.** The attachment object (jcid 0x60035) carries what a paragraph's | |
| 31 | file does, except LayoutAlignmentInParent; its position (0x14001c14/15), | |
| 32 | LayoutAlignmentSelf 9, and the column's LayoutMaxWidth/Height (54 by about 63 | |
| 33 | points) with the size not set by the user. A file inserted through COM has | |
| 34 | neither alignment nor size. | |
| 35 | - **Moves.** Dragging rewrites the position alone, on the grid; the | |
| 36 | modification time stays. | |
| 37 | - **Payloads.** Both copies' embedded-file objects, and both icons, name one | |
| 38 | payload in the file data store. |
corpus/attachment-floating/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/attachment-floating/candidate/Open Notebook.onetoc2 differ |
corpus/attachment-floating/candidate/files.one created| Binary files /dev/null and b/corpus/attachment-floating/candidate/files.one differ |
corpus/attachment-floating/cold/commands.jsonl created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} | |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-45BB9354 -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} | |
| 3 | {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} |
corpus/attachment-floating/cold/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-45bb9354", "hostname": "ONE-M6-45BB9354"} |
corpus/attachment-floating/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment created| Binary files /dev/null and b/corpus/attachment-floating/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment differ |
corpus/attachment-floating/cold/read/78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f.attachment created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | Snowbound put this file on the page. |
corpus/attachment-floating/cold/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-45BB9354", | |
| 5 | "cold": true, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/attachment-floating/cold/read/hierarchy.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{6A8583F5-6FC0-4A26-A608-93D1BD212D43}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-29T18:39:16.000Z" color="#B49EDE"><one:Section name="files" ID="{993FE3E6-D8AC-092B-1A41-2B1D1969C115}{1}{B0}" path="C:\one-tests\runs\capture\notebook\files.one" lastModifiedTime="2026-09-29T18:39:16.000Z" color="#8AA8E4"><one:Page ID="{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}" name="Floating files" dateTime="2026-09-29T18:31:15.000Z" lastModifiedTime="2026-09-29T18:39:05.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/attachment-floating/cold/read/page-000.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Floating files - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/attachment-floating/cold/read/page-000.png created| Binary files /dev/null and b/corpus/attachment-floating/cold/read/page-000.png differ |
corpus/attachment-floating/cold/read/page-000.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}" name="Floating files" dateTime="2026-09-29T18:31:15.000Z" lastModifiedTime="2026-09-29T18:39:05.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" authorInitials="A" lastModifiedBy="Author" lastModifiedByInitials="A" lastModifiedTime="2026-09-29T18:31:15.000Z" objectID="{993FE3E6-D8AC-092B-1A41-2B1D1969C115}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="96.23243713378906" height="13.4277172088623"/><one:OEChildren><one:OE creationTime="2026-09-29T18:31:15.000Z" lastModifiedTime="2026-09-29T18:31:15.000Z" objectID="{993FE3E6-D8AC-092B-1A41-2B1D1969C115}{24}{B0}" alignment="left"><one:T><![CDATA[Floating files]]></one:T></one:OE></one:OEChildren></one:Outline><one:InsertedFile pathCache="C:\Users\clover\AppData\Local\Microsoft\OneNote\14.0\OneNoteOfflineCache_Files\e2f66eb2-07db-4c10-ab3d-8c06ad8742dd.bin" pathSource="/Users/snowbound/Documents/large.bin" preferredName="large.bin" lastModifiedTime="2022-06-18T04:26:43.000Z" objectID="{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{4}{B0}"><one:Position x="288.0" y="140.3999938964844" z="2"/><one:Size width="54.0" height="62.99999237060547"/></one:InsertedFile><one:InsertedFile pathCache="C:\Users\clover\AppData\Local\Microsoft\OneNote\14.0\OneNoteOfflineCache_Files\6db07d21-03c2-4cd2-8cf3-e517e1598974.txt" pathSource="/Users/snowbound/Documents/float 🦀.txt" preferredName="float 🦀.txt" lastModifiedTime="2022-06-18T04:26:41.000Z" objectID="{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{2}{B0}"><one:Position x="342.0" y="284.3999938964844" z="1"/><one:Size width="54.0" height="63.0"/></one:InsertedFile></one:Page> |
corpus/attachment-floating/cold/read/payloads.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "sha256": "1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a", | |
| 4 | "name": "large.bin", | |
| 5 | "object": "{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{4}{B0}", | |
| 6 | "kind": "InsertedFile", | |
| 7 | "page": "{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}", | |
| 8 | "bytes": 2097152 | |
| 9 | }, | |
| 10 | { | |
| 11 | "sha256": "78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f", | |
| 12 | "name": "float 🦀.txt", | |
| 13 | "object": "{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{2}{B0}", | |
| 14 | "kind": "InsertedFile", | |
| 15 | "page": "{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}", | |
| 16 | "bytes": 37 | |
| 17 | } | |
| 18 | ] | |
| \ No newline at end of file |
corpus/attachment-floating/cold/run.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "notebook": "/private/tmp/snowbound-attachments/export", | |
| 3 | "expected_pages": 1, | |
| 4 | "author": null, | |
| 5 | "author_timeout_seconds": 600, | |
| 6 | "inspect": false, | |
| 7 | "collect_notebook": true, | |
| 8 | "base": { | |
| 9 | "file": "win7-office-base.qcow2", | |
| 10 | "format": "qcow2", | |
| 11 | "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", | |
| 12 | "virtual_size": 68719476736 | |
| 13 | }, | |
| 14 | "scripts": { | |
| 15 | "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", | |
| 16 | "read.ps1": "22bc760f955ef59bb5f0ccfd55271f8d868674084127fbe219e1048bd3777f35" | |
| 17 | } | |
| 18 | } |
corpus/attachment-floating/cold/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/attachment-floating/cold/scripts/read.ps1 created+144| ... | ... | @@ -0,0 +1,144 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | # A paragraph's file is named by its paragraph, one on the page by itself. | |
| 103 | $owner = if ($file.ParentNode.LocalName -eq 'Page') { $file } else { $file.ParentNode } | |
| 104 | $payloads += @{ page = $id; object = $owner.GetAttribute('objectID'); | |
| 105 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 106 | sha256 = $hash; bytes = $bytes.Length } | |
| 107 | } | |
| 108 | $index++ | |
| 109 | } | |
| 110 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 111 | $all = '' | |
| 112 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 113 | [xml]$finalTree = $all | |
| 114 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 115 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 116 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 117 | } | |
| 118 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 119 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 120 | } catch { | |
| 121 | $failure = $_ | |
| 122 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 123 | throw | |
| 124 | } finally { | |
| 125 | try { | |
| 126 | try { | |
| 127 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 128 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 129 | $app.CloseNotebook($notebookId, $false) | |
| 130 | } | |
| 131 | } catch { | |
| 132 | if ($null -eq $failure) { throw } | |
| 133 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 134 | } | |
| 135 | } finally { | |
| 136 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 137 | } | |
| 138 | $app = $null | |
| 139 | [GC]::Collect() | |
| 140 | [GC]::WaitForPendingFinalizers() | |
| 141 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 142 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 143 | } | |
| 144 | } |
corpus/attachment-floating/cold/source.json created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1604, | |
| 5 | "sha256": "46e74799daaccb7e37abf1696a1920ce47bb39bd721a2e7e15af8d0f6db7416f", | |
| 6 | "mtime_ns": 1790706678169228316 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "files.one", | |
| 10 | "bytes": 2106848, | |
| 11 | "sha256": "cb063e9a0656899d90d4f95e469d9fec1c72bc259d44f673071d6b03e55ab241", | |
| 12 | "mtime_ns": 1790706678168913736 | |
| 13 | } | |
| 14 | ] |
corpus/attachment-floating/cold/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
corpus/attachment-floating/native/files.one created| Binary files /dev/null and b/corpus/attachment-floating/native/files.one differ |
corpus/attachment-insert/README.md created+42| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | # Attaching files from the editor | |
| 2 | ||
| 3 | `candidate/` is `crates/canvas/tests/attachment_insert.rs`'s section: the editor | |
| 4 | attaches `notes 🦀.txt` (30 bytes, with the icon OneNote stored for a text file) | |
| 5 | at the end of "Before the file", "After the file" is typed where the caret | |
| 6 | lands, and `large.bin` (2 MiB of noise, with the canvas's blank-page icon, as | |
| 7 | a host without system icons stores it) is attached after that. Each step is its | |
| 8 | own edit of the ops the editor recorded, sealed into one appended revision. | |
| 9 | `cold/` is a fresh OneNote 2010 read: both files in place, and the bytes OneNote | |
| 10 | materializes to open them (`read/*.attachment`) hash as written. Opened by | |
| 11 | double click and saved by Save As in a lab clone, both matched too. | |
| 12 | `tools/test_attachment_insert.py` checks the capture without a VM. Regenerate | |
| 13 | with `CANVAS_ATTACHMENT_EXPORT` set to a directory while running the test, then | |
| 14 | `tools/native_runner.py DIR COLD --expected-pages 1 --collect-notebook | |
| 15 | --screenshots`. | |
| 16 | ||
| 17 | `without-icon.png` is OneNote opening a file stored without the icon: it draws | |
| 18 | a broken picture, so the editor always stores one. | |
| 19 | ||
| 20 | ## What OneNote 2010 does | |
| 21 | ||
| 22 | Observed in a lab clone on 2026-09-29: | |
| 23 | ||
| 24 | - **Storage.** Every attachment goes into the section's own file data store, | |
| 25 | whatever its size (1 KiB to 300 MiB, through COM and through Insert > Attach | |
| 26 | File, on a local folder and on a share). OneNote never wrote a `_onefiles` | |
| 27 | folder. Identical bytes are stored once per section: a second attachment of | |
| 28 | the same file, or the same icon, references the first payload. Snowbound | |
| 29 | stores each attachment's bytes anew, which OneNote reads the same. | |
| 30 | - **Icon.** Beside each file it stores a 32-pixel PNG of the file type's | |
| 31 | system icon, drawn at 24 points, over the name without its extension. | |
| 32 | - **Attach File and drops.** At the caret, the paragraph splits there: the file | |
| 33 | follows the text before the caret (or replaces an empty first half) and the | |
| 34 | text after it follows the file with the caret, as an empty paragraph at the | |
| 35 | end of one. A drop on text does the same at the drop point, after asking | |
| 36 | whether to attach a copy (the default) or link to the original. Attached or | |
| 37 | dropped on blank page, the file becomes a page-level object at that point. | |
| 38 | - **Open.** A double click or Open warns once ("Opening attachments could harm | |
| 39 | your computer and data") and opens a copy from | |
| 40 | `%TEMP%\OneNote\14.0\NT\N\` under the file's own name. The context menu | |
| 41 | offers Open, Save As (in the source's folder, under its name), Insert as | |
| 42 | Printout, Open Original and Copy Link to Original. |
corpus/attachment-insert/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/attachment-insert/candidate/Open Notebook.onetoc2 differ |
corpus/attachment-insert/candidate/files.one created| Binary files /dev/null and b/corpus/attachment-insert/candidate/files.one differ |
corpus/attachment-insert/cold/commands.jsonl created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} | |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-DDCBCDBF -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} | |
| 3 | {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} |
corpus/attachment-insert/cold/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-ddcbcdbf", "hostname": "ONE-M6-DDCBCDBF"} |
corpus/attachment-insert/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment created| Binary files /dev/null and b/corpus/attachment-insert/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment differ |
corpus/attachment-insert/cold/read/6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3.attachment created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | Snowbound attached this file. |
corpus/attachment-insert/cold/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-DDCBCDBF", | |
| 5 | "cold": true, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/attachment-insert/cold/read/hierarchy.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{A826269C-EF84-4056-BC12-661022F3D9A3}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-29T17:40:55.000Z" color="#B49EDE"><one:Section name="files" ID="{3B2C3DA1-9735-0B63-2EE7-6F168478D766}{1}{B0}" path="C:\one-tests\runs\capture\notebook\files.one" lastModifiedTime="2026-09-29T17:40:55.000Z" color="#8AA8E4"><one:Page ID="{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}" name="Before the file" dateTime="2026-09-29T17:38:22.000Z" lastModifiedTime="2026-09-29T17:40:55.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/attachment-insert/cold/read/page-000.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Before the file - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/attachment-insert/cold/read/page-000.png created| Binary files /dev/null and b/corpus/attachment-insert/cold/read/page-000.png differ |
corpus/attachment-insert/cold/read/page-000.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}" name="Before the file" dateTime="2026-09-29T17:38:22.000Z" lastModifiedTime="2026-09-29T17:40:55.000Z" pageLevel="1" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" authorInitials="A" lastModifiedBy="Author" lastModifiedByInitials="A" lastModifiedTime="2026-09-29T17:38:22.000Z" objectID="{3B2C3DA1-9735-0B63-2EE7-6F168478D766}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="103.4619598388672" height="166.2831420898437"/><one:OEChildren><one:OE creationTime="2026-09-29T17:38:22.000Z" lastModifiedTime="2026-09-29T17:38:22.000Z" objectID="{3B2C3DA1-9735-0B63-2EE7-6F168478D766}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the file]]></one:T></one:OE><one:OE creationTime="2022-06-18T04:26:41.000Z" lastModifiedTime="2022-06-18T04:26:41.000Z" objectID="{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{5}{B0}" alignment="left"><one:InsertedFile pathCache="C:\Users\clover\AppData\Local\Microsoft\OneNote\14.0\OneNoteOfflineCache_Files\ad2e52ea-b4aa-4d5a-9687-f694b5a347d4.txt" pathSource="/Users/snowbound/Documents/notes 🦀.txt" preferredName="notes 🦀.txt"/></one:OE><one:OE creationTime="2022-06-18T04:26:41.000Z" lastModifiedTime="2022-06-18T04:26:42.000Z" objectID="{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{3}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the file]]></one:T></one:OE><one:OE creationTime="2022-06-18T04:26:43.000Z" lastModifiedTime="2022-06-18T04:26:43.000Z" objectID="{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{8}{B0}" alignment="left"><one:InsertedFile pathCache="C:\Users\clover\AppData\Local\Microsoft\OneNote\14.0\OneNoteOfflineCache_Files\8d5146be-280a-481f-8c76-92bb915c8c80.bin" pathSource="/Users/snowbound/Documents/large.bin" preferredName="large.bin"/></one:OE><one:OE creationTime="2022-06-18T04:26:43.000Z" lastModifiedTime="2022-06-18T04:26:43.000Z" objectID="{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{6}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/attachment-insert/cold/read/payloads.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "sha256": "6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3", | |
| 4 | "name": "notes 🦀.txt", | |
| 5 | "object": "{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{5}{B0}", | |
| 6 | "kind": "InsertedFile", | |
| 7 | "page": "{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}", | |
| 8 | "bytes": 30 | |
| 9 | }, | |
| 10 | { | |
| 11 | "sha256": "1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a", | |
| 12 | "name": "large.bin", | |
| 13 | "object": "{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{8}{B0}", | |
| 14 | "kind": "InsertedFile", | |
| 15 | "page": "{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}", | |
| 16 | "bytes": 2097152 | |
| 17 | } | |
| 18 | ] | |
| \ No newline at end of file |
corpus/attachment-insert/cold/run.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "notebook": "/private/tmp/snowbound-attachments/candidate", | |
| 3 | "expected_pages": 1, | |
| 4 | "author": null, | |
| 5 | "author_timeout_seconds": 600, | |
| 6 | "inspect": false, | |
| 7 | "collect_notebook": true, | |
| 8 | "base": { | |
| 9 | "file": "win7-office-base.qcow2", | |
| 10 | "format": "qcow2", | |
| 11 | "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", | |
| 12 | "virtual_size": 68719476736 | |
| 13 | }, | |
| 14 | "scripts": { | |
| 15 | "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", | |
| 16 | "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" | |
| 17 | } | |
| 18 | } |
corpus/attachment-insert/cold/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/attachment-insert/cold/scripts/read.ps1 created+142| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 103 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 104 | sha256 = $hash; bytes = $bytes.Length } | |
| 105 | } | |
| 106 | $index++ | |
| 107 | } | |
| 108 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 109 | $all = '' | |
| 110 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 111 | [xml]$finalTree = $all | |
| 112 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 113 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 114 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 115 | } | |
| 116 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 117 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 118 | } catch { | |
| 119 | $failure = $_ | |
| 120 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 121 | throw | |
| 122 | } finally { | |
| 123 | try { | |
| 124 | try { | |
| 125 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 126 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 127 | $app.CloseNotebook($notebookId, $false) | |
| 128 | } | |
| 129 | } catch { | |
| 130 | if ($null -eq $failure) { throw } | |
| 131 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 132 | } | |
| 133 | } finally { | |
| 134 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 135 | } | |
| 136 | $app = $null | |
| 137 | [GC]::Collect() | |
| 138 | [GC]::WaitForPendingFinalizers() | |
| 139 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 140 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 141 | } | |
| 142 | } |
corpus/attachment-insert/cold/source.json created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1604, | |
| 5 | "sha256": "8e2a1137f47d89425a05053b93f0789be77f219ae2bd4d8bb0bdd1b052a93074", | |
| 6 | "mtime_ns": 1790703504369465697 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "files.one", | |
| 10 | "bytes": 2108416, | |
| 11 | "sha256": "3d60c4a8628ec91e1b96c0a4f43391a2e78206ac2d0918dc0c9b3ef8e851c8de", | |
| 12 | "mtime_ns": 1790703504369204448 | |
| 13 | } | |
| 14 | ] |
corpus/attachment-insert/cold/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
corpus/attachment-insert/without-icon.png created| Binary files /dev/null and b/corpus/attachment-insert/without-icon.png differ |
corpus/custom-tags/README.md created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | # Customized note tags | |
| 2 | ||
| 3 | `candidate/` is the editor's output for `custom_tags_store_what_customize_tags_stores` | |
| 4 | in `crates/canvas/tests/custom_tags.rs`: on a section created in Rust, four tags of a | |
| 5 | customized list are applied through `Formatting::Tag`, each definition storing the tag's | |
| 6 | place in the list as its action type: "Snow check" (Green Star, 61, dark red text on Sky | |
| 7 | Blue), "Snow task" (Green Check Box 1, 48, once open and once checked), "Snow circle" | |
| 8 | (Blue Circle 2, 31) and "Snow ink" (no symbol, blue text). Definitions carry the | |
| 9 | NoteTagPropertyStatus bits OneNote's Customize Tags writes: hasLabel, hasFontColor and | |
| 10 | hasHighlightColor where set, hasIcon for a symbol. | |
| 11 | ||
| 12 | `cold/` is a fresh OneNote 2010 read: the four `TagDef` entries keep their type, symbol, | |
| 13 | name and colours, and the tags their completion state. `tools/test_custom_tags.py` | |
| 14 | checks this without a VM. Regenerate with `ONESTORE_CUSTOM_TAGS_EXPORT` set to a new | |
| 15 | absolute directory while running the test, then cold-open it with | |
| 16 | `tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook --screenshots`. | |
| 17 | ||
| 18 | `native/shapes.one` is OneNote 2010's own work in a lab clone: | |
| 19 | ||
| 20 | - "Shapes 1-72" and "Shapes 73-143" tag a paragraph with every NoteTagShape, set through | |
| 21 | COM (its definitions store NoteTagPropertyStatus 0). | |
| 22 | - "Custom tags" holds a tag made with New Tag (Snow check, as above), applied at the top | |
| 23 | of the list (action type 0), then after moving it below To Do (a second definition, | |
| 24 | action type 1), with To Do applied from both places. The UI stores status 15 and 9. | |
| 25 | - "Cells" tags a paragraph `cR-C` with the symbol picked from row R, column C of Modify | |
| 26 | Tag's symbol gallery, which `crates/snowbound/src/tags.rs` lays out. | |
| 27 | ||
| 28 | OneNote keeps the tag list in `%APPDATA%\Microsoft\OneNote\14.0\Preferences.dat`, written | |
| 29 | when it exits; Snowbound keeps its own in its settings. |
corpus/custom-tags/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/custom-tags/candidate/Open Notebook.onetoc2 differ |
corpus/custom-tags/candidate/custom-tags.one created| Binary files /dev/null and b/corpus/custom-tags/candidate/custom-tags.one differ |
corpus/custom-tags/cold/commands.jsonl created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} | |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-DEFAE25B -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} | |
| 3 | {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} |
corpus/custom-tags/cold/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-defae25b", "hostname": "ONE-M6-DEFAE25B"} |
corpus/custom-tags/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/custom-tags/cold/notebook/Open Notebook.onetoc2 differ |
corpus/custom-tags/cold/notebook/custom-tags.one created| Binary files /dev/null and b/corpus/custom-tags/cold/notebook/custom-tags.one differ |
corpus/custom-tags/cold/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-DEFAE25B", | |
| 5 | "cold": true, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/custom-tags/cold/read/hierarchy.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{451C75B9-B066-454B-AA65-839980D633FA}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-29T17:59:43.000Z" color="#B49EDE"><one:Section name="custom-tags" ID="{4453BE32-4DE3-0B80-027A-F5D2AD14B507}{1}{B0}" path="C:\one-tests\runs\capture\notebook\custom-tags.one" lastModifiedTime="2026-09-29T17:59:43.000Z" color="#8AA8E4"><one:Page ID="{93E3B613-F542-0CAA-2043-B7DF4FDCC322}{1}{B0}" name="Custom symbol and colours" dateTime="2026-09-29T17:57:38.000Z" lastModifiedTime="2026-09-29T17:59:43.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/custom-tags/cold/read/page-000.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Custom symbol and colours - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/custom-tags/cold/read/page-000.png created| Binary files /dev/null and b/corpus/custom-tags/cold/read/page-000.png differ |
corpus/custom-tags/cold/read/page-000.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{93E3B613-F542-0CAA-2043-B7DF4FDCC322}{1}{B0}" name="Custom symbol and colours" dateTime="2026-09-29T17:57:38.000Z" lastModifiedTime="2026-09-29T17:59:43.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:TagDef index="0" type="0" symbol="61" fontColor="#800000" highlightColor="#00CCFF" name="Snow check"/><one:TagDef index="1" type="1" symbol="48" fontColor="automatic" highlightColor="none" name="Snow task"/><one:TagDef index="2" type="2" symbol="31" fontColor="automatic" highlightColor="none" name="Snow circle"/><one:TagDef index="3" type="3" symbol="0" fontColor="#0000FF" highlightColor="none" name="Snow ink"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" authorInitials="A" lastModifiedBy="Author" lastModifiedByInitials="A" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{4453BE32-4DE3-0B80-027A-F5D2AD14B507}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="163.1240997314453" height="67.13858032226562"/><one:OEChildren><one:OE creationTime="2026-09-29T17:57:38.000Z" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{4453BE32-4DE3-0B80-027A-F5D2AD14B507}{24}{B0}" alignment="left"><one:Tag index="0" completed="true" disabled="false" creationDate="2026-09-29T17:57:40.000Z" completionDate="2026-09-29T17:57:40.000Z"/><one:T><![CDATA[Custom symbol and colours]]></one:T></one:OE><one:OE creationTime="2025-08-18T14:13:20.000Z" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{26222B6E-6954-0C04-3EB2-D2C7B252F53F}{2}{B0}" alignment="left"><one:Tag index="1" completed="false" disabled="false" creationDate="2026-09-29T17:57:40.000Z"/><one:T><![CDATA[Custom check box]]></one:T></one:OE><one:OE creationTime="2025-08-18T14:13:20.000Z" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{26222B6E-6954-0C04-3EB2-D2C7B252F53F}{5}{B0}" alignment="left"><one:Tag index="1" completed="true" disabled="false" creationDate="2026-09-29T17:57:40.000Z" completionDate="2026-09-29T17:57:40.000Z"/><one:T><![CDATA[Custom check box, checked]]></one:T></one:OE><one:OE creationTime="2025-08-18T14:13:20.000Z" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{26222B6E-6954-0C04-3EB2-D2C7B252F53F}{7}{B0}" alignment="left"><one:Tag index="2" completed="true" disabled="false" creationDate="2026-09-29T17:57:40.000Z" completionDate="2026-09-29T17:57:40.000Z"/><one:Tag index="3" completed="true" disabled="false" creationDate="2026-09-29T17:57:40.000Z" completionDate="2026-09-29T17:57:40.000Z"/><one:T><![CDATA[Two custom tags]]></one:T></one:OE><one:OE creationTime="2025-08-18T14:13:20.000Z" lastModifiedTime="2025-08-18T14:13:20.000Z" objectID="{26222B6E-6954-0C04-3EB2-D2C7B252F53F}{11}{B0}" alignment="left"><one:T><![CDATA[Untagged]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/custom-tags/cold/read/payloads.json created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | [ | |
| 2 | ||
| 3 | ] | |
| \ No newline at end of file |
corpus/custom-tags/cold/run.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "notebook": "/private/tmp/snowbound-custom-tags/candidate", | |
| 3 | "expected_pages": 1, | |
| 4 | "author": null, | |
| 5 | "author_timeout_seconds": 600, | |
| 6 | "inspect": false, | |
| 7 | "collect_notebook": true, | |
| 8 | "base": { | |
| 9 | "file": "win7-office-base.qcow2", | |
| 10 | "format": "qcow2", | |
| 11 | "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", | |
| 12 | "virtual_size": 68719476736 | |
| 13 | }, | |
| 14 | "scripts": { | |
| 15 | "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", | |
| 16 | "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" | |
| 17 | } | |
| 18 | } |
corpus/custom-tags/cold/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/custom-tags/cold/scripts/read.ps1 created+142| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 103 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 104 | sha256 = $hash; bytes = $bytes.Length } | |
| 105 | } | |
| 106 | $index++ | |
| 107 | } | |
| 108 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 109 | $all = '' | |
| 110 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 111 | [xml]$finalTree = $all | |
| 112 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 113 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 114 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 115 | } | |
| 116 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 117 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 118 | } catch { | |
| 119 | $failure = $_ | |
| 120 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 121 | throw | |
| 122 | } finally { | |
| 123 | try { | |
| 124 | try { | |
| 125 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 126 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 127 | $app.CloseNotebook($notebookId, $false) | |
| 128 | } | |
| 129 | } catch { | |
| 130 | if ($null -eq $failure) { throw } | |
| 131 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 132 | } | |
| 133 | } finally { | |
| 134 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 135 | } | |
| 136 | $app = $null | |
| 137 | [GC]::Collect() | |
| 138 | [GC]::WaitForPendingFinalizers() | |
| 139 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 140 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 141 | } | |
| 142 | } |
corpus/custom-tags/cold/source.json created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1612, | |
| 5 | "sha256": "2a6009814bda4756e9d474b02433f075adf6071d7cc0650da8502e46519dea1c", | |
| 6 | "mtime_ns": 1790704660620140274 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "custom-tags.one", | |
| 10 | "bytes": 8296, | |
| 11 | "sha256": "91b1b0c54326b6e686b467e74fe51e080b8bd748c4d62f328072c3ace2a3c226", | |
| 12 | "mtime_ns": 1790704660619982358 | |
| 13 | } | |
| 14 | ] |
corpus/custom-tags/cold/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
corpus/custom-tags/native/shapes.one created| Binary files /dev/null and b/corpus/custom-tags/native/shapes.one differ |
corpus/page-background/README.md+12-8| ... | ... | @@ -2,12 +2,16 @@ |
| 2 | 2 | |
| 3 | 3 | `candidate` is `backgrounds_go_on_and_off_pages_that_have_content` in |
| 4 | 4 | `crates/snowbound/src/manage.rs` (`SNOWBOUND_BACKGROUND_EXPORT`), on a copy of |
| 5 | `rule-lines/native/notebook`: the Page Color menu's ops give the ruled "Standard" page | |
| 6 | Ivy's art (`manage::art_ops`), give "Wide" the art and take it away again, and colour | |
| 7 | "SmallGrid" `0x00c8d8e8`, a COLORREF outside OneNote's menu. | |
| 5 | `rule-lines/native/notebook`: the Page Color menu's steps through the editor | |
| 6 | (`CanvasEditor::set_paper`), each stored as one edit. "Standard" takes Ivy's art; "Wide" | |
| 7 | takes it and has it taken away; "College" takes it and undoes; "MediumGrid" takes it, | |
| 8 | undoes and redoes; "LargeGrid" takes Ivy, then Tulips in its place, and undoes; "SmallGrid" | |
| 9 | is coloured `0x00c8d8e8`, a COLORREF outside OneNote's menu; "VeryLargeGrid" is coloured | |
| 10 | Lemon and takes Sparks' art. | |
| 8 | 11 | |
| 9 | `cold` is its fresh OneNote 2010 read: "Standard" keeps its rule lines under one | |
| 10 | background image (`backgroundImage="true"`), "Wide" keeps its lines and no image, and | |
| 11 | "SmallGrid" reads back `color="#E8D8C8"` and shows it under its grid. `read/page-006.png` | |
| 12 | shows OneNote drawing the art over the rule lines. `tools/test_page_background.py` | |
| 13 | checks this without a VM. | |
| 12 | `cold` is its fresh OneNote 2010 read: every ruled page keeps its lines; "Standard", | |
| 13 | "MediumGrid" and "LargeGrid" show one Ivy background image (`backgroundImage="true"`), | |
| 14 | "Wide" and "College" none; "SmallGrid" reads back `color="#E8D8C8"` under its grid, and | |
| 15 | "VeryLargeGrid" `#FDFDDD` under Sparks. `read/page-003.png` shows OneNote's opaque art | |
| 16 | hiding the rule lines, and `read/page-005.png` its white hiding the Lemon page colour. | |
| 17 | `tools/test_page_background.py` checks this without a VM. |
corpus/page-background/candidate/Rules.one| Binary files a/corpus/page-background/candidate/Rules.one and b/corpus/page-background/candidate/Rules.one differ |
corpus/page-background/cold/commands.jsonl+1-1| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 1 | {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-5EF70348 -ExpectedPages 11 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 11 pages.\r\n", "stderr": "", "error": null} | |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-0EA92EBC -ExpectedPages 11 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 11 pages.\r\n", "stderr": "", "error": null} | |
| 3 | 3 | {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\read -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} |
corpus/page-background/cold/machine.json+1-1| ... | ... | @@ -1 +1 @@ |
| 1 | {"name": "m6-5ef70348", "hostname": "ONE-M6-5EF70348"} | |
| 1 | {"name": "m6-0ea92ebc", "hostname": "ONE-M6-0EA92EBC"} |
corpus/page-background/cold/read/environment.json+1-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | { |
| 2 | 2 | "powershell": "5.1.14409.1005", |
| 3 | 3 | "schema": "xs2010", |
| 4 | "hostname": "ONE-M6-5EF70348", | |
| 4 | "hostname": "ONE-M6-0EA92EBC", | |
| 5 | 5 | "cold": true, |
| 6 | 6 | "onenote": "14.0.4763.1000" |
| 7 | 7 | } |
corpus/page-background/cold/read/hierarchy.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{4E9398E9-CFD7-43A8-9875-DD25B12F2FD4}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-29T08:41:31.000Z" color="#EE9597"><one:Section name="Rules" ID="{8582DE68-7D51-02A8-377A-D6E00C2FCD89}{1}{B0}" path="C:\one-tests\runs\capture\notebook\Rules.one" lastModifiedTime="2026-09-29T08:41:31.000Z" color="#B49EDE"><one:Page ID="{31D0327B-94B7-0B5E-3E7D-671F54D55CEC}{1}{B0}" name="None" dateTime="2026-09-29T07:53:40.000Z" lastModifiedTime="2026-09-29T08:41:31.000Z" pageLevel="1"/><one:Page ID="{6A54865E-CC2F-025D-27BF-0929DFE646F9}{1}{B0}" name="Narrow" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:10.000Z" pageLevel="1"/><one:Page ID="{37C93113-BD98-0DA1-0616-DB06D76B9DE7}{1}{B0}" name="College" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:16.000Z" pageLevel="1"/><one:Page ID="{859BC3C1-DA73-0094-187D-081E0BF4B451}{1}{B0}" name="Standard" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:21.000Z" pageLevel="1"/><one:Page ID="{5E86B14D-2EA5-0CEE-01DC-77C26A83BFD9}{1}{B0}" name="Wide" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:26.000Z" pageLevel="1"/><one:Page ID="{D343E8F3-DEB6-0F1E-1EB1-8B94E7382757}{1}{B0}" name="SmallGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:31.000Z" pageLevel="1"/><one:Page ID="{5BFB88BD-0BC5-02DF-1E44-A227794FE0A1}{1}{B0}" name="MediumGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:20.000Z" pageLevel="1"/><one:Page ID="{9E24A66D-E1BF-0540-29D9-FD682782DED2}{1}{B0}" name="LargeGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:42.000Z" pageLevel="1"/><one:Page ID="{51A21F45-2AB8-0652-01E3-91BE86AD2DC6}{1}{B0}" name="VeryLargeGrid" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:57:47.000Z" pageLevel="1"/><one:Page ID="{B41FFCB9-1864-06FB-3F3A-EA4F5FDC10B9}{1}{B0}" name="ColorRed" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:04:15.000Z" pageLevel="1"/><one:Page ID="{ED27E4E9-4094-0D2D-34A2-4C38706F92CD}{1}{B0}" name="Hidden" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:06:00.000Z" pageLevel="1"/></one:Section></one:Notebook> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{2212F41E-1AE1-4A1B-8CF6-BFC2B788D85F}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-29T17:14:47.000Z" color="#EE9597"><one:Section name="Rules" ID="{5682FD55-9DBC-0B28-3629-4C30A2B59DA6}{1}{B0}" path="C:\one-tests\runs\capture\notebook\Rules.one" lastModifiedTime="2026-09-29T17:14:47.000Z" color="#B49EDE"><one:Page ID="{E2D01146-745A-02DE-3F2E-FDCFFA4F0CC3}{1}{B0}" name="None" dateTime="2026-09-29T07:53:40.000Z" lastModifiedTime="2026-09-29T17:14:47.000Z" pageLevel="1"/><one:Page ID="{B954A563-2CC2-0BDD-26EC-93F9717C16D6}{1}{B0}" name="Narrow" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:10.000Z" pageLevel="1"/><one:Page ID="{E4C9122E-5D75-0421-0745-41D679F1CDC8}{1}{B0}" name="College" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:16.000Z" pageLevel="1"/><one:Page ID="{569BE0FC-3A9E-0914-192E-92CEA56EE47E}{1}{B0}" name="Standard" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:21.000Z" pageLevel="1"/><one:Page ID="{8D869270-CE48-056E-008F-ED12C419EFF6}{1}{B0}" name="Wide" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:26.000Z" pageLevel="1"/><one:Page ID="{0043CBCE-3E5B-069E-1FE2-114449A27778}{1}{B0}" name="SmallGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:31.000Z" pageLevel="1"/><one:Page ID="{88FBAB80-EB28-0B5F-1F17-38F7D7D5B08E}{1}{B0}" name="MediumGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:20.000Z" pageLevel="1"/><one:Page ID="{4D248550-0152-0CC0-288A-67B889188EFD}{1}{B0}" name="LargeGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:42.000Z" pageLevel="1"/><one:Page ID="{82A23C78-CA55-0FD2-00B0-0B6E28377DE9}{1}{B0}" name="VeryLargeGrid" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:57:47.000Z" pageLevel="1"/><one:Page ID="{671FDF84-F889-0F7B-3E69-709FF1464096}{1}{B0}" name="ColorRed" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:04:15.000Z" pageLevel="1"/><one:Page ID="{3E27C7D4-A079-04AD-35F1-D6E8DEF5C2E2}{1}{B0}" name="Hidden" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:06:00.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/page-background/cold/read/page-000.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "None - Microsoft OneNote", | |
| 9 | "title": "SmallGrid - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-000.png| Binary files a/corpus/page-background/cold/read/page-000.png and b/corpus/page-background/cold/read/page-000.png differ |
corpus/page-background/cold/read/page-000.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{31D0327B-94B7-0B5E-3E7D-671F54D55CEC}{1}{B0}" name="None" dateTime="2026-09-29T07:53:40.000Z" lastModifiedTime="2026-09-29T08:41:31.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{AF1767DC-FDC6-0BBC-0FC1-316E92D0839F}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[None]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{0043CBCE-3E5B-069E-1FE2-114449A27778}{1}{B0}" name="SmallGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:31.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="#E8D8C8"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="12.0"/><one:Vertical color="#CAEBFD" spacing="12.0"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{9E4CBF4E-A49D-00FD-2FCB-F75A0257886A}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[SmallGrid]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-001.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "College - Microsoft OneNote", | |
| 9 | "title": "Hidden - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-001.png| Binary files a/corpus/page-background/cold/read/page-001.png and b/corpus/page-background/cold/read/page-001.png differ |
corpus/page-background/cold/read/page-001.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{37C93113-BD98-0DA1-0616-DB06D76B9DE7}{1}{B0}" name="College" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:16.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="23.76000022888184"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:06.000Z" objectID="{FB694B2B-9D3B-0B6E-15D1-EB5B9DE960D8}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[College]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{3E27C7D4-A079-04AD-35F1-D6E8DEF5C2E2}{1}{B0}" name="Hidden" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:06:00.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="#D4F9F2"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#FFFFFF" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:05:45.000Z" objectID="{F6B39D95-0451-0BED-1146-B16FB8FB1A09}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Hidden]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-002.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "VeryLargeGrid - Microsoft OneNote", | |
| 9 | "title": "LargeGrid - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-002.png| Binary files a/corpus/page-background/cold/read/page-002.png and b/corpus/page-background/cold/read/page-002.png differ |
corpus/page-background/cold/read/page-002.xml+352-1| ... | ... | @@ -1,2 +1,353 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{51A21F45-2AB8-0652-01E3-91BE86AD2DC6}{1}{B0}" name="VeryLargeGrid" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:57:47.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="56.69279861450195"/><one:Vertical color="#CAEBFD" spacing="56.69279861450195"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{20F7B97B-36FA-0C22-1185-45868B492B11}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[VeryLargeGrid]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{4D248550-0152-0CC0-288A-67B889188EFD}{1}{B0}" name="LargeGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:42.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="42.51959991455078"/><one:Vertical color="#CAEBFD" spacing="42.51959991455078"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{CA15E073-7FE8-06C1-0EE0-50F6F3E1F940}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[LargeGrid]]></one:T></one:OE></one:Title><one:Image format="jpg" backgroundImage="true" lastModifiedTime="2026-09-29T17:12:33.000Z" objectID="{8A22F7F7-C573-08F0-2797-F47A607B1A73}{5}{B0}"><one:Position x="-27.0" y="-3.599999666213989" z="0"/><one:Size width="174.5" height="640.0" isSetByUser="true"/><one:Data>/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB | |
| 3 | EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL | |
| 4 | EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU | |
| 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID | |
| 6 | BAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHB | |
| 7 | FVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2 | |
| 8 | d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna | |
| 9 | 4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1 | |
| 10 | EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ0 | |
| 11 | 4SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeI | |
| 12 | iYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery | |
| 13 | 8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAOS8f+KbvwbFYamkX2nThL5V3EB8wDfdcH2wRjoc1 | |
| 14 | 5GYYqpg+SqleN7NfkzGtV9jDnauup0el6nbazp8F7aSCW2nUOjjuK9KlVhXgqkHdM1TUldbHK/Cf | |
| 15 | /kW7r/sIXH/odeblf8F/4pfmcGB/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAB | |
| 16 | QAUAFABQAUAFABQBW1HTrbVrGazu4lntpl2PG3QisqtKFaDpzV0xNKSszmNP0uP4X+ENUZLh7y1t | |
| 17 | vMuIUkGCgxwme/PfjrXlU6X9l4ao+bmSu1/l95lCEcPTstlf/Mz/AIM3Dt4GM03+ta6mZsjHzFuf | |
| 18 | 1qMok3hOZ73Zx4Bv2Lb7s7yBzJErHqRXtxd0ekSVQBQAUAFABQAUAFABQAUAFABQAUAFABQAUAFA | |
| 19 | BQAUAFABQAUAFADZHWJGdyFVQSWPQCk2oq7ASGZLiJZInWSNxlXQ5DD1BpRkppSi7oDn4/FunXmt | |
| 20 | 3+g6gi211EC3lXGDHPFjO5SeDx1HbB64zXmrGUZ1Z4asrNd9mt7/AHb/APAJU1z8iepj/C8ed4Su | |
| 21 | 9ufmv7gjj/brDLfew7a/mf5nDg1+6a83+Z3ESeXGq+gr2krI9AZ9oVpNi5Y98dBU82tkFyarAKAC | |
| 22 | gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgBCAwIIyD2NG4HkFrf6j8J/GI0+5Ly+F7+ | |
| 23 | U+Q7ZIhLHoD2weCO45r46E6mUYn2Uv4Unp5f8N17rU83mnh63LL4JbeT7eh1/ivwtb+MNQSVVMd1 | |
| 24 | puDFdREK3mnDKpJB+UDkjB+9x3B9PGYVY6p7u8Ovnul6Ld+unVP03Sg+WUt07ry/4crfClhH4cnE | |
| 25 | gDZv7gb8cZ31eWaUXzfzP8zz8F/Dfq/zOz3gswUHaOCe1evfsd5IkaxjCgCrSS2AfTAKACgAoAKA | |
| 26 | CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgDGu/GGi6fqDWN3qMFrdLg+XO2zIPcE4B/CuGeNw | |
| 27 | 9Obpzmk/PQlyinZtXIPFHiPw/p2j+bq9xay2UudsbYk83H91ec1ni8VhIUr12nF9N7+iCUlTXNJ2 | |
| 28 | RzHgb4mw6/dw6fY6PfTb2Lz3QVRHGWJJJ56DOB344FeTl2ZKs1ShTbb1b0sr6v5L/hjOOJhXn7id | |
| 29 | u9tC98Kw58Pz44UX9znHf5+ld2Wp+yf+KX5nLgf4b9WdkgckqfuZyDjH4V6yu9D0AnhkY5jkK+3a | |
| 30 | nJN7CYsJlHEij/eFC5uoK/UmqxhQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQBzPjfw | |
| 31 | Hp/jiwEVyPJuowfJukHzJ7e49q83G4CnjY2lpJbP+uhz16EMRHlmeG6r4L8WeA5J9kU01kwKtLbf | |
| 32 | vIXU/wB5e34ivhcRgsTg+bmT5e61X9eqPHjRxuDlej7y/rp/ke3fD3xUfFGjbpdPk027gIWWExFE | |
| 33 | J7FMjpx07fln7bLsX9apax5Wt9LL5f1oe3SqSqwUpRafZlD4USn+w7iMDj7dckn/AIGajLH+6a/v | |
| 34 | S/M48C/3b9X+Z2lyWWIsnVea9eV7aHoMdE/mRq3qKad1cY+mAUAFABQAUAFABQAUAFABQAUAFABQ | |
| 35 | AUAFABQAUAFABQAUAFAFW81Oz05d11dQWw9ZpAv86xqVqdL+JJL1dgMbU/Eujalpt3bQavYtPJEw | |
| 36 | QC4TO7HHf1xXn4jFYetRnCFRXadtVv0KpySktTctLpb2yhuYsMssYkT3BGRXo06iq01Uj1VxNcrs | |
| 37 | zi/g3E8fhKTzcGYXk6uR6huf1rycoTWH97e7POwX8N+rO6r2z0BEQRrgdKSVgHUwCgAoAKACgAoA | |
| 38 | KACgAoAKACgAoAKACgAoAKACgAoAKACgDP1zSTrenvai8urAtz51pJsce2fSubEUPrEOTmcfNOzB | |
| 39 | q6seN678AtXed5bPVYr7P/P2WV/z5zXyFbIq6d6clL10f6niVsulUd1Ub9f6/Qx4PgP4mlkCv9jh | |
| 40 | Xu7TZx+QrmWS4xuzSXzOVZVUvrJfieo/DTwZL4Ma7tLjUp7qbapEOSIAp/iVT3yCPw96+hyvBywc | |
| 41 | pwnLXt0s+q873X/Dnv0KHsKaXM5evQn+E/8AyLd1/wBhC4/9Drpyv+C/8UvzOXA/w36s7OvXPQFo | |
| 42 | AKACgBKAFoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAKl9ZtP5csJCXMOTG56c9V | |
| 43 | Psf8D1Arnq03O0oaSW3+T8n/AMHdFRdtHscp8JCW8MXBKlSb+4yp7fP0rz8qd6Dv/M/zPNwX8N+r | |
| 44 | OuvSRASOCCDmvVnsd72FtbgTp/tDqKIyuCdyerGFAFO5nxcxoOgIzWUnrYlvWxcrUoKAEoAiNyvm | |
| 45 | BF+Zj6dqhy6IVyarGFABQAUAFABQAUAFABQAUAFABQAUAFABQAnSgBEdZEDIwZT0KnINJNSV0Bxv | |
| 46 | wn/5Fu6/7CFx/wCh15OV/wAF/wCKX5nn4H+G/VnYTp5kLj2r1ZK6PQ3MuGUwyBh26iudOxinZmur | |
| 47 | BlBHQ11J3NhaAMkHzbr6tXNuzLeRrV0mohOBk0AZ11eGQ7U4T+dYSlczcuxLYQYHmHv0qoLqOKLt | |
| 48 | alhQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAHnGreDtd8Ma2dU8JzB7WVt1xpMr4jJ77AeB+hH6 | |
| 49 | V8zVwWIwlX22B2e8en9fijknTqxnz03p1T2+XY0PhE5k8LTsylGa+uCUPVTv6V3ZU70G3/M/zMsF | |
| 50 | /Dfqztq9g9Ay7uHyZT/dPIrnkrMykrFuwfdDj+6a0g9C47FqtCijbW5FyxPRTxWMY6kJa3L1bFlL | |
| 51 | UJsARjvyaym+hEmQWtqZmyeEH61EY3JjG5pgYGB0FdBqLQAUAFABQAUAFABQAUAFABQAUAFABQAU | |
| 52 | AFABQBn63rdr4esGvb0ulshAeRELbM9yBzj3rnxGIhhoe0qbfeJtRTb2OX+EM6XHhaaSNgyPezup | |
| 53 | 9QX4NedlUlKg2u7/ADODAu9J+rO4r2T0BksSyptYUmrhuRW0P2bcCc5PGKiK5dxJWJdzHov5mqu+ | |
| 54 | wxcsP4R+Bo1AAwJx0PoadwImtFklLtk+1TyXd2KxMBgYHAqxi0AJmgAoAWgAoAKACgAoAKACgAoA | |
| 55 | KACgAoAKACgAoAjngjuYXilRZIpFKsjDIYHqDUyippxkrpgeffC61+zeF5BCCI4rydBjsA/FeHls | |
| 56 | eWg0tk3+Z5eDVqbt3Z21tfHIWTkf3q9iM+56Cl3LrHHTqa1bNAC4+vrQkA6mAUAV/PEk/ljkDqaz | |
| 57 | vd2FfUmU84PX+dWuwxJZREhY0N2DYzpbySQ8HaPQVg5NmTk2JHBLMeAfqaSTYWbLtvaiHkks1bRh | |
| 58 | Y0SsWKsYUAFABQAUAFABQAUAFABQAUAFABQAUAFAHFfCf/kW7r/r/uP/AEOvIyz+C/8AFL8zz8D/ | |
| 59 | AA36s657SKQ524PtXqOCZ3tJj4l2jvgcDPpTihjLgyoN0ZzjquKUrrVCd+hFFqAJxINvuKlT7kqR | |
| 60 | PcSbIGYenBq5PS5T0INPiIBkPfgVEF1Jiiy/GD6GrfcsdVAJsA7ClZAOpgFABQAUAFABQAUAFABQ | |
| 61 | AUAFABQAUAFABQAUAFAHF/Cf/kW7r/sIXH/odeRlf8F/4pfmefgf4b9WdnXrnoABgUALQBC0Ecjs | |
| 62 | WUcVHKmwsL+78j/pnRpbyAEuI3OFYfSmpJivcc/QD1IoYx1UBEbuJT98fhU8yFdDkmST7rA+1Ckm | |
| 63 | MkqgCgAoAKACgAoAKACgAoAKACgAoAKAGSyLDE8jnCICxPoBSk1FNvoBwt98bfC1lKUW4nuSDgmG | |
| 64 | E4/XFeFPO8JF2V36L/OxxTxmHg7OX5mn4e+Jvh7xNcLb2l8EuW4WGdSjMfQZ4P4GunD5nhsTLkjK | |
| 65 | z7PT/gGtPEUq2kJXKvwn/wCRbuv+whcf+h1OV/wX/il+Zz4H+G/VnaV656AUAFAFK/kK/IP4utYz | |
| 66 | ZMn0LFumyBVPpWkVZDWhXmsB95G2+xqJQ7EuPYltllHEnOOhpxv1KV+pPWgynLZeYMpww4IPeseS | |
| 67 | +xMo3KjwvEfmUj3rNpozs0TQXrRnDfMv6iqUmilLuaEciyLuU5Fbp3NB1MAoAKACgAoAKACgAoAK | |
| 68 | ACgAoAKAOC8Y/B3R/FMz3URbTb5+WlhAKOfVl/qMV4OLyijiXzwfLL8Pu/yOKvg6WI1krPujk7D9 | |
| 69 | ndku1a71oGBSDiCHa5/Ek4/WvLhkEub36mnktTjhlcIyu5v8jsvhFH5XhadMs22+uBljkn5+5r2s | |
| 70 | qXLQa/vP8zpwP8N+rO1ZwgyxAHvXrt2PQI1uo3farZJ9BU8yYromqxkBtg9wZG5x0FRy3d2K2tyX | |
| 71 | YO2R9DTsMAgB9frTsA6mAxZVZioI3DtSumADh2HrzQtwHUwGmND1UH8KVkAqoqdAB9KEkgHUwCgA | |
| 72 | oAKACgAoAKACgAoAKACgAoAKACgDg/hhdCDw1cgcub+4/wDQ68XLpWov/FL8zzcG7Un6s6XEly/d | |
| 73 | j/KvR1kdmsi/a2ogGTy571tGNjRKxYqyhMjOM8jtRcBaAEyM47+lAC0AZ19GUl3jv396wmrMzl3J | |
| 74 | bS58xlVvvDIz604yu9SoyuXK2KCgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAEoA4f4TW8f/AAjU | |
| 75 | +Ru231wAWOT9+vGyuK9g/wDE/wAzz8F/DfqzuAAowBge1eyegLQBXu7nyFwPvn9KiUrEt2ItOyzS | |
| 76 | Mec4qICiXa2LM6ecpebv7vFYN+8Zt2ZoA5Fbmg2WMSoVPek1cNzMkje1lHtyD61ztOLMbcrNSN/M | |
| 77 | QMO4roTujYdTAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA4v4T/wDIt3X/AGELj/0OvIyv+C/8 | |
| 78 | UvzPPwP8N+rO0r1z0BCcCgDImkM0pb16Vyt3MW7s0rWHyYgO55NdEVZGqViaqGZV4u24euaW5lLc | |
| 79 | 0of9Un0FdEdjUfTAjmQSBVYZBNTJXDcIYvJTbnIHSiKsGxJVAFABQAUAFABQAUAFABQAUAFABQBz | |
| 80 | /h3xZDr2qa3ZABJNNuPKP+0uPvfmGH4V52FxixFSrT/kdvl/w9yIzjJtLp/w5e/tC5vObG3Vou1x | |
| 81 | cMVVh6qACWHucA9ia09tUq/wY6d3ovkt3+CfRm/Ko/Ewxq8fzbrKb/pmFeM/99Zb+VH+1R191+Wq | |
| 82 | /G7/ACD3PMsWd+l0zRlWhuE5eGT7w9/Qj3HH45ranWVT3WrSW6f9arzRLjbXocr8J/8AkW7r/sIX | |
| 83 | H/odeflf8F/4pfmebgf4b9WdpXrnoEN2+y3b34qJ7CexBZ2m3DuOewqYx6smMbF2tSwoAo6hFl0Y | |
| 84 | d+KxmtbkSVy6o2qB6CtVoWLTAY331/E1L3QD6oAoAKACgAoAKACgAoAKACgAoAKACgDyjwp4dng+ | |
| 85 | KniiGTJsZAJ3x0k3MGVD7cnP0IPBNfH4XDN5jWpv4d353aaX+fe1tmzmowlSr1J20la34nq9fYHS | |
| 86 | FAFe8slu1Xlo5UOY5U+8h/z26GsalJVLdGtn1RSlY5H4S5Hhm43EFvt9xkjjJ315uV39g7/zP8zz | |
| 87 | sF/Dfqztq9g7xGUNjIzjmi1wFoAKACgBrKG69uaTVwHUwCgBg5kPsKnqA+qAKAI5bhIfvHn0qXJI | |
| 88 | Tdiq+o/3U/E1m59ieciN/KT2H4VPOyedifbpf7w/KjmYczHLqEg67T+FPnY+clTUR/En5VSn3Hzo | |
| 89 | mS7if+LHsapTTKTTJutWMWgAoAKAOO0PU9OTXdVhuLK3spLS6EFvcvDjePLU/wCsI5YknvnBr57D | |
| 90 | VcPDEVIzgouLsnby/m7/ADvqa805J32/roa/ijxdpvhDTftmoTbVbiONOXkPoo/r0r1MVjKWEhz1 | |
| 91 | HvsurOapUhRjzTdkeX3Hx81C6Mr6docf2eMZaWeQkKP9rGAPzr5eefVXd06at53f+R5yx7nrTpu3 | |
| 92 | duyOa8Q/GXxF4jRLW0xp6twRZbt8h+vUfQYrgr5tisQuW/L6f57/AHHFWzCpVfJQX+fy/q/oem/B | |
| 93 | KBl8Gq0u7zVuJVYN1B3c596+nyeP+zK/dnfl6ao673Z6FXunphQAUAFABQAUAFABQAxORn15qV3A | |
| 94 | dVAUrm95Kx/99VjKfYhy7FInJ5rMyCkAUAFABQA6ONpGwoJppXGlcvQWKpy/zH07VsodzVRsWq0K | |
| 95 | FoAKACgDzt9EXX9VbRpdPe70a1t4mguNQjcvG5dvMO6QbmJAAx06dq+UdH61V+rcjcIpWck7rXXV | |
| 96 | 6u+1ttuhfIl8aTX366nRXF94WNzHpNxLpfnW+ES0l8vMfTgKenavXlPA3VCfLeOlnbQzcle19Sv4 | |
| 97 | j+Hem+KBbx3Mk8NjBylnalY4i394gDJOOP5dTU4jLaWJaTbUV0VkvXYzq0oVlyz1Rd0DwRofhk7t | |
| 98 | O06KGXGPOI3P/wB9HJrfD4DD4bWnDXvu/wARwpQpK0FYyPhP/wAi3df9hC4/9Drnyv8Agv8AxS/M | |
| 99 | 48D/AA36s7SvXPQCgAoAKACgAoAKAGv0x3PFJ9gFAwKYEV0kkkeEIHr71Ek3sJmY8Txn5lIrBqxi | |
| 100 | 00NpCCgA60ASx20knRT9TxVKLZSiy1Fp4HLnPsK0UO5aii0iKgwoAHtWiVix1MAoAKACgAoA4S98 | |
| 101 | ezaV8UYdBuAv2C6t0ETY5WU7sHPcH7v4D3rwJ4+VLMPq8/haX3/8HY5pV+WuqL6q/wAyLx78OrPx | |
| 102 | 3dS9NP1eBAY5xys8Z6bh7HI45HHYipx2AhjpyS92a69GvP0enlp3QsTg4YiKb0fcyvAw8X+B71NM | |
| 103 | 1izm1HR3bZHc2583yPfj5tvqCOO3cHkwTxuAmqVaDcO61t+tvL7vPLDxxFJ8lT3l0f8Ameq19Yd5 | |
| 104 | xfwn/wCRbuv+whcf+h15GV/wX/il+Z5+B/hv1Z2leuegFABQAUAFABQAUAMX5ju/KktdQH0wCgBC | |
| 105 | M0AMaCNuqL+VTyoBBbRD+BaOVBYeqKvRQPoKdkgHUwCgAoAKACgAoAKACgDzn4meEpr/AMQeG9at | |
| 106 | ELPbXkMU+3rsMgIb8Dkf8Cr5rNMLKdaliIdGk/v0Zy1qLqVKc1vF/gdxqkEjRpcQLuubc7kUfxr/ | |
| 107 | ABJ+I/UA9q9vEQk0qkPijt5918/zs+h2xfR7MtW86XUEc0Tbo5FDKfUGt4TVSKnHZktWdmSVYji/ | |
| 108 | hP8A8i3df9hC4/8AQ68jK/4L/wAUvzPPwP8ADfqztK9c9AKACgAoAKACgBrfMdv50nroAtMBaACg | |
| 109 | AoAKACgAoAKACgAoAKACgAoAKACgAoAKAM2w/wBE1C6s/wCA/wCkRD0DE7h+DZP/AAMVxUf3dWdH | |
| 110 | p8S+e6+/X5mktUpfIZd+JbG112z0cuZL+5DMIk52IATub0HGKc8VThWjQ3lL8PNmPNHm5b6mB8J/ | |
| 111 | +Rbuv+whcf8AodcmV/wX/il+Zw4H+G/VnaV656AUAFABQAUANY46dTSbAUDAoSsAtMAoAKACgAoA | |
| 112 | KACgAoAKACgAoAKACgAoAKACgAoA5/xdqY8PWqawV3C2SVWX1UoWA/76RPzrysfV+rKOJ7XX3q/5 | |
| 113 | pFppQlfor/ccR8G/D9/eXt94r1bcbm9BWEyDkqTkt7DgAew+leTk+HqTnLGVt3t593/keVg4TfNX | |
| 114 | qby/I6P4T/8AIt3X/YQuP/Q69TK/4L/xS/MrA/w36s7SvXPQCgAoAKAEJx/hSbsBFJMsHLnlvSpb | |
| 115 | 5dwbsM+3xep/KlzonmRItzE/Rx+NVzJjumSVQxaACgAoAKACgAoAKACgAoAKACgAoAKACgAoAyfF | |
| 116 | MEdxoV0Jo1liQLIyOMghWDEEenFefj4xlh5cyulZ/c7/AKGlP4rGqqhVAAAA4AHavQStojM4z4T/ | |
| 117 | APIt3X/YQuP/AEOvIyv+C/8AFL8zz8D/AA36s7SvXPQCgAoAazY9z6UmwADHJ60WAhvo98Ge681E | |
| 118 | 1oTJXRmVgYhQBJHO8R+Vj9KpNopSaL0F6svDfK36VrGd9zRSuWa0KCgAoAKACgAoAKACgAoAKACg | |
| 119 | AoAKACgClrSeZo98vXdBIMf8BNc2KXNQqLyf5Fw0kie0fzbWF+u5FP6VrTfNBPyJejOR+E//ACLd | |
| 120 | 1/2ELj/0OvMyv+C/8UvzPOwP8N+rO0r1z0AoAbuzwv5+lK/YBQuP8aErALTAQjIxQBk3EPkyle3a | |
| 121 | uZqzMZKxHUkhQAUAXLS8x8jn6E1rGVtGaxl0ZfrYsKACgAoAKACgAoAKACgAoAKACgBgmjL7A6lh | |
| 122 | /CDzU88W7XHZlXWn8vR75s42wSHP/ATWGKfLQqPyf5FQ1kie0j8q1hTptRR+la01ywS8iXqzkfhP | |
| 123 | /wAi3df9hC4/9DrzMs/gv/FL8zzsD/DfqzsTIB05r1eZHoABv6nj0FG+4DulUAtABQAUAQ3MAnTH | |
| 124 | 8Q6GplG4mrmW6FGKkYIrn2MWrCUhBQAUAX7K53jY3UdD61tCXQ1i7lytSwoAKACgAoAKACgAoAKA | |
| 125 | CgAoA4/xn8M9N8XSfagz6fqYGBdwcFh6MO/16142Nyuji3zr3Z9/8zCrRjV6tPutynpHh7XvD2iX | |
| 126 | djqmsJq1rNsghLIfNUs4XGSeRg9DnHrXJHD4nD0JUq1Tmi7Jb31aX3am2GhOm/flzW8rHeV9IM4f | |
| 127 | 4Wxeb4YugGKn7fcdP9+vGy1c1B/4pfmefgv4T9WdDNA8R+YH616DTR1NMYCVPBI+lSK5PFeyRnk7 | |
| 128 | h71ak0UpF2G5SfpwfQ1spJmidyaqGFABQBDcWyzr6MOhqJRuJq5myxNC2GFYNWMWrDKQgoAVWKMC | |
| 129 | OopjWhrQSiaMN+Yroi7o2TuSVQwoAKACgAoAKACgAoAKACgAoAzdS/f6jp1t/ttcMP8AZQY/9CZf | |
| 130 | yrir+/Vp0/Nv5Jf5tGkdItmlXaZnE/CpxH4ZumY4A1C4/wDQ68fLHag/8T/M8/Bfwn6s7JZo5OAy | |
| 131 | n2r1uZM9Aa1rE/VB+HFLlTFZDPsEXofzo5ELlQ5bOJTkA5+tHIh2SJqsYtABQAUANdFkXDAEe9Jq | |
| 132 | 4FKbTyOYzn2NZOHYhx7FR0ZDhgQfes9jNqwlIRZsZtku09G/nWkHZmkX0NKtzQKACgAoAKACgAoA | |
| 133 | KACgAoAQkKMkgAdzRsBm6d/pt7c33BjP7iEjoUX7zfi2fwUVw0P3tSVbpsvRbv5v8EjSXupRNOu4 | |
| 134 | zOE+GYz4SvP+whcf+h14uXf7vL/E/wAzzsH/AAn6v8zoK7zpJI7mSLo3HoapNopSaLsF8snDfK36 | |
| 135 | Vqp33NFJMs1oULQAUAFABQAUAFADWUOMEAj3oauBBJYRv0yp9qzcETyogbT3U5VgcfhUcjJ5bbF5 | |
| 136 | CSoyMHuK2WxoOpgFABQAUAFABQAUAFABQB5r4l8Ia54t1uCPVNTW2093Jj0+xLcRj7zuxxk9B0PL | |
| 137 | DpXyuLwmKxlWMK07J7RXZbt7frq1sZOi6kuaUvdXRaX9Wei2trFY2sVvBGsUESBEReiqBgAV9PCE | |
| 138 | aUVCCslojUlqwOH+Fy7/AArej1vrn/0OvGy3WhL/ABS/M8/Bfwn6s3a7joCgAoAs2t4YjtblP5Vp | |
| 139 | GVjSMu5oghhkcg1uaC0AFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQBm6Z/pV3eXh/ifyI/ | |
| 140 | ZEJB/Nt34Y9K4qH7yc6z72Xov+Df8DSWiUTSrtMwoA4r4T/8i3df9f8Acf8AodeRln8F/wCKX5nn | |
| 141 | 4H+G/Vm9cR+VMy+/FdzVmdUlZkdSSFABQBasrny22N909PatIytoaRfQ0a3NAoAKACgAoAKACgAo | |
| 142 | AKACgAoAKACgAoAKACgAoAQnAoAzvDo/4kdk3eSISn6t8x/nXFgv92g+6v8AfqaVPjZpV2mYUAcX | |
| 143 | 8J/+Rbuv+whcf+h15GV/wX/il+Z5+B/hv1Z1F/DuXzB1HX6V6U11O2SKFYmQUAFABQBpWU/mx4P3 | |
| 144 | lreDvobRd0Wa0KCgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAIbttlrM3ojH9KzqO0G/Ia3INGXZ | |
| 145 | o9ivpBGP/HRWWFVqFNeS/IqfxMi0nX7TWrrUYLZi7WM/2eU9t20E4/PH1BpUcTCvKcYfZdn/AF+B | |
| 146 | mmnsaVdQzi/hP/yLd1/2ELj/ANDryMr/AIL/AMUvzPPwP8N+rOzI3DB6GvXPQMq5gMEmP4T0NczV | |
| 147 | mYyViKpJCgAoAkt5fJlB7dDVJ2ZUXZmtXSbC0AFABQAUAFABQAUAFABQAUAFABQAUAFABQA1lDqV | |
| 148 | YBlIwQehFJpNWYEF7L9h06eSNQPJiZlUcDgcCsqj9lSk49F+Q+p5d+z550+na3dykt51wvzn+JsE | |
| 149 | sf8Ax4V8zkCk41JPrb79b/meXl7lKnKcusn+h3/i3xdp/g3SzeX7nk7Y4U+/K3oB/M9q9/F4ung4 | |
| 150 | c9T5Lqzuq1YUY883ocx8H7iS/wDCxkXMaS3U0pA7ZbOM152VSdShfu2/xOHAPmpXXdnoPSvePTGT | |
| 151 | QiZNp/A+lS1cTVzLliaF9rVztWMWrDKQgoAKANS0k8yAeo4rog7o3TuierGFABQAUAFABQAUAFAB | |
| 152 | QAUAFABQAUAFABQB5z8M/iuni1/7O1FUttWUfKV4WYDrgdiPT8q+dy3NPrT9lV0l08/+CefhcWsR | |
| 153 | 7slaS6HoF2kslu6wtGsh4BlUsvvkAjt7171RSlFqFr+eq/NHoqyepznhfSv+EWsZNO0/7NdwW0p8 | |
| 154 | 6OIFJVZsN3JBOCOOOMV5OEpSwcHSp2kovW2ju9eraeluw4wpxXLFWOO8R/DjVPiN4pfULu9jtdGT | |
| 155 | EduqHfJsHX5eiknOc8joRxXl1sBXzGu60pJQ6dXb06fPY8zE4N4ir+8laK6HQ/B63W08JywpnZHe | |
| 156 | zoueuA2K9XKYqGHcV0b/ADDAK1Jpd2dzXsnohQBFPAs64PXsfSpcbiauZk0LQthh9D61g1YyasMq | |
| 157 | SQoAuac+HZfXmtYPU0gX62NAoAKACgAoAKACgAoAKACgAoAKACgAoAKAMK68D6HeatFqb6fGl/E4 | |
| 158 | kWeIlGLA5ydpGfxrz54DDTqKq4+8ne60/Ihwg5c7Sv3M7xJ8RbPQL5bD7Jey3bnCsLVzH7kHHzY/ | |
| 159 | 2c/UVyYvNIYefslFuXo7f8H5FOUYtKV/kmzN0e9tfA+q61d69qlvB/atws9ujBlYrtHJQ5K8nHP9 | |
| 160 | 2ubD1I4GpVnippc7TW+3pq1vbXsZtxpNylKyb6+iRsw66HTVLnQ401lTHHcRxQTKFZ23Kw3dBwgO | |
| 161 | PUn1rqhiburLCrn0Tsn1d0/yvb/M6JO8FJa7mJ8JLuc+FXZoCrveTs6c/KxbkVGVzl9Xu11f5nlY | |
| 162 | Fv2T06s76KQyDlCn1r207npElUAUANeNZFwwyKTVwM+4smiyV+Zf1FYyjYycexWrMgltW2XCfXFV | |
| 163 | HRlR3Nauk2CgAoAKACgAoAKACgAoAKACgAoAKACgAoA848AeGPGdjeLca5rb/Zh1sywmZ/qx+7+F | |
| 164 | fN4DC4+E1OvUaXa97/5fI48PCvHWtO/lb9Tur+2kM0F1AoeaDcPLJxvRsbgD2PAI+mOM5HtVoSco | |
| 165 | 1Ybx6d09/ns16W03XfFrVM4vwSJptAeDWdNn1C8F5OyJcW2QoLn+Nht65714OAk5UHCvTcpczteP | |
| 166 | n3em9+pMISS99rd/m7fgbV94h0zwS0H9qyraPfMcPGhMSbQAEyBwAD1wM8njOK9B16WAa9vo59lo | |
| 167 | rW0+X46vS9iatWELczsttSh8I3WXwxcOhDK1/cEMOhG+jK2nQbX8z/M4MD/Dfqztq9g9AKACgAoA | |
| 168 | KAK89mkvI+VvUVm4JkuKZRkgeBskcDuKyaaIs0aoORXSai0AFABQAUAFABQAUAFABQAUAFABQAUA | |
| 169 | FAHK/ETxvB4J0Rpid17NlLaIdS3dvoP8B3rycyxqwdH3fie3+fyMa1eGHjzz+7uZPwl1rW/FFrf6 | |
| 170 | vq0pMErrHbRKoVFC53ED6kDPtXJlNXEYnnrVno9F203McLVqVoe0mrX29C94l8fDw/400PRmRTDe | |
| 171 | g+a56qWO2PH4g5+tb4rMPq+Lp0Oj3+eiNZ1lCpCm/tXNHx14Qh8aeH5bCRhHMD5kEpH3HHTPt2P1 | |
| 172 | rqx2EWMouns90/MK1KNem6cupzXwZL2HhJLeXAKXMqMB2IbFcGUXhh+V92cWX+7S5X3Z6LXvnqC0 | |
| 173 | AFABQAUAFACEZFAABgUALQAUAFABQAUAFABQAUAFABQAUAFABQAUAePz+Bb/AOJ3i+61TVWey0a3 | |
| 174 | kNvBCeJXRT2H8OTkknnnpXxywdTNMRKtV92Cdl3svyPOrYSVerzVfhWy7+fz/I9ZsrKDTrSK1tol | |
| 175 | gt4VCJGgwFA7V9dTpxpRUIKyR6CVlZHkfxU0ia6+JvhVkB2ztGgI7FJMt+hr5DNaUpY6lbrb8Ged | |
| 176 | iYOWIoyXd/hqex19kekcR8K4ll8NXQYZ/wCJhcf+h14+WJOg/wDFL8zz8F/CfqztVXaoGc47mvXW | |
| 177 | h6A6mAmecUALQAUAJQAtABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAGb4f/wCQWhP3jJKW/wB7 | |
| 178 | zGz+ua4sH/BXq/vu7mlT4jSrtMypdaXbXt5Z3U0Yae0Zmhb+6WXaf0rGdGFScaklrHb56AW62A4v | |
| 179 | 4T/8i3df9hC4/wDQ68jK/wCC/wDFL8zz8D/DfqztK9c9AKAKd45hmjkH0NZS0dyZOxaRg6hh0NaJ | |
| 180 | 3KBmCKSegobsBWs5DNLI59gKzg7tslO5brUoKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAzdNP2 | |
| 181 | a9vrQ9n+0R+6vkn/AMeDfmPWuKh7lSdLzuvR/wDBuaS1SkaVdpmFABQBxfwn/wCRbuv+whcf+h15 | |
| 182 | GV/wX/il+Z5+B/hv1Z2IlQnG4Z9M1610egOpgR3EPnx7e/UVMldCauMswyxbWGCDilDYFsF8SLc4 | |
| 183 | 7kUT2CWwtnD5UPPU8miCsgSsierGFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAUNTjeJoryFS8k | |
| 184 | GdyKMl4z94AdzwCPcY71yV4uLVaCu4/iuv8AmvS3U0i7+6y5FKk8SSRsHjcBlZTkEHoa6YyU0pRd | |
| 185 | 0zNq2jH1QBQBxfwn/wCRbuv+whcf+h15GV/wX/il+Z5+B/hv1Z1txarPz0b1r1JRud7VyCNLi3bg | |
| 186 | b19M1CUokpNF3Py5x+FaljRIpUEEYPSi6AXKv6GjRgKTgUwKkl/822Ndx9TWTn2J5uxYh37cyYye | |
| 187 | w7Vcb9SiSqAKACgAoAKACgAoAKACgAoAKACgAoAKACgDPeCXTpGltk823YlpLYdQTyWTtz3Xv1GD | |
| 188 | nPG4Tovmpq8eq/Vfquu6s730upaMs2t5Fexb4X3AHBBBBU+hB5B9jXRTqRqrmg/6810IacdyetBH | |
| 189 | F/Cf/kW7r/sIXH/odeRlf8F/4pfmefgf4b9WdpXrnoBQAlAFe3jaHfjlTyPr6VlFNBawLBI775G5 | |
| 190 | HRR0o5W9WJEroJ48HIB9Ku3MhtBFbpD91efWhRSFaxJVDCgAoAKACgAoAKACgAoAKACgAoAKACgA | |
| 191 | oAKACgCnd6alxL50bNb3QGBPF1I9GHRh7H8MVzVKCm+eLtLuv17r1+RalbToQ/2jNY8X8WEH/LzA | |
| 192 | CYz7kclP1HvWft50tK607rb5rdfivMfKpfCc58JWD+Gbkggg39wQR3+euTK3eg2v5n+Z5eC/hv1Z | |
| 193 | 2tewegRSzqh27vm7AVDlYBXZyn7vGT69qbba0AZDb7Ad7FmbqaSjbcENt2aMOj87eh9RSi7aMSJH | |
| 194 | kFvGC2T9Kd+VDbI1v4ie4+opc6J5kTq4cZUgj2q07lDqYBQAUAFABQAUAFABQAUAFABQAUAFABQA | |
| 195 | UAFABQAUAcV8Jhjw1c/9f9x/6HXkZX/Bf+KX5nn4L+G/VnX3DOF2xglj39K9STeyO9kdvaeW29zu | |
| 196 | f+VKMbasSVidyEBbOMcmqehQBmYZAU575ouwELBXAZhuPQUddQHkZGDVAV3sY2PGV+lZuCJcUwis | |
| 197 | /JbKyMKFC3UaVizWgwoAKACgAoAKACgAoAKACgAoAKACgAoAKAIJL6CK7itXlVbiZWaOMnlwuM4+ | |
| 198 | mRWbqQjNU29Xt8gJ60AKAOL+E/8AyLd1/wBhC4/9DryMr/gv/FL8zz8D/DfqztK9c9AQnAoApXzk | |
| 199 | Io7tyaxmTN6Fm1/490+laR2GtiO9hMseR95amauKSuOtJvOi5+8ODTg7oad0T1YwoAKACgAoAKAC | |
| 200 | gAoAKACgAoAKACgAoAKACgAoA4f4s+HLvW/D8d3pvmDU9Ok+0QmIkOR/EFx36H8K8PNsNOtRVSl8 | |
| 201 | UHdW387fn8jlxNOU6f7v4lqix8MPGreM/D/mXGBqFs3lXAAxuOOGx2z/ADBrTLMa8ZR974o7/o/m | |
| 202 | GGre3p8z32fqdLdXbiUW9uA9wRklvuxL/eb+g6n2GSPQqVHf2dP4vwXm/wBF18ldrsS6vY5T4SDb | |
| 203 | 4ZuASWIv7j5j1Pz15+Vq1B/4n+Z5uC/hv1Z21ewegMb5mA/E1L10AqXyGSaNR34rOauyJK9i4q7F | |
| 204 | AHbitUrFjqYDFjCMxHG7rSSs7gPpgFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAHlvw18dtq3 | |
| 205 | jPxDps8m5Jrh57XJ7KdpUf8AAQp/A18rlmNdTE1acnpJtr+vT8jho4n2tWpTfR6em39ep20WlWWl | |
| 206 | apdPpdrHFqF4Fa4dc7FAJw7LnGTk4xgt+BI9j2VOjVl7CNpy37Jd2v6b+Ta9CMEry/pmraWiWcWx | |
| 207 | SzEnc7vyzt3JPr/+oYArtp01TVl9/Vvu/wCvJaCbucl8J/8AkW7r/sIXH/odeblf8F/4pfmedgf4 | |
| 208 | b9WdmTtGfSvX2PQEQYGT1PNJABUFg2ORRbqA6mAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUA | |
| 209 | FABQA2TPltjrg0pbMEfJvhnVptF8YWd7G2JEucEvnGCcHP4E1+VYerKjVjUjumfKUJ+zxt31bX3s | |
| 210 | +rLO0WziKgl3Y75JG+87ep/QewAA4Ar9QpU1SjbdvVvu+/8AWy0Wh9a3csVsScX8J/8AkW7r/sIX | |
| 211 | H/odeRlf8F/4pfmefgf4b9Wdi3zMB6cmvVersegPqgCgAoAKACgAoAKACgAoAKACgAoAKACgAoAK | |
| 212 | ACgAoAKACgAoAgvLtLK3aV8kDACr1Yk4Cj3J4rKpUVKLlL+vL5jS5nY+V/FuhXGheMrmzljCO04k | |
| 213 | RVORtc5AB79cfhX5hiaM6FWVOa1/zPlcTS9njbLZtNfNn1bGCI1B6gCv1NbI+pY6mBxXwnOPDV1/ | |
| 214 | 1/3H/odeRln8CX+KX5nn4H+G/VnZoOMnqea9ZHoDqYBQAUAFABQAUAFABQAUAFABQAUAFABQAUAF | |
| 215 | ABQAUAFABQAUAZbSpc6i80rqlrZHYCxwDKRyfwBAHuzelcHMqlVyk/dhp/28/wDJaLzb7Gnwxt3/ | |
| 216 | ACM/xN4EsPE+q6XqE+UubGVX3AZ8xAc7D7Z5z9fWs8VgKeKqQqy0cX967HNOlCpKMpLWOx0lemah | |
| 217 | QBxPwpGfDlyO39oXGf8AvuvHyzWi1/el+ZwYH+G/VnbV7B3hQAUAFABQAUAFABQAUAFABQAUAFAB | |
| 218 | QAUAFABQAUAFABQAUAFAHiX/AAkVx4y+IGl6BZTM+k2Fx500g4+0Oh3tI3sW6D3r4ZVpYvFU8NB3 | |
| 219 | hF39Wndt+r2POliHXxfs4fDHf5dPkz22vuT0RCQoyTgepobsAUAcZ8J/+Rbuv+whcf8AodeRlf8A | |
| 220 | Bf8Ail+Z5+B/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAcB8Kf | |
| 221 | hwfBllJd3uG1W5UBwDkRJ12g9z3J/wAOfAyvLvqkfaVPjf4Lt/mcOEw31eLvrJ7nf1753EN3aQ31 | |
| 222 | tLb3EaywyqVeNxkMD2qJwjUi4TV0wPMn8K+LvA2q+d4euW1jR2bJ0+5lG5R/dG79CD9RXy7wmNwF | |
| 223 | Tmwr54dm/wCvvRw+zr053py5o9nv8mbnwiYv4XnYqUJvrglT1Hz9K9PKneg3/ef5k4L+G/Vnb17B | |
| 224 | 6AUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQBz8vi+zu7FrjR7i31YxyIjxW8q | |
| 225 | knccKM5wCTjr715c8fBw58O1OzSaXnovxKhaezv6HDXfxA8etfNHb+EjEgPCPG78f7wIFeNLMMxc | |
| 226 | 7Ro2+Tf4nmyrYnmtGlp6o77wvf61qFkX1rTIdNm4wkc/mZ+oxx+Zr3sJUxNSN8RBRfr+nT7zui24 | |
| 227 | 3krP7zbrvKOL+E//ACLd1/2ELj/0OvIyv+C/8UvzPPwP8N+rO0r1z0AoAKACgAoAKACgAoAKACgA | |
| 228 | oAKACgAoAKACgAoAKACgAoAKACgDAgbT5riztLMQW1orGZEQBBMy9Ai8ZAJBLdOAOeceTF0ak4U6 | |
| 229 | VlFa9rtdu6W7fpa+ttbci8zfr1jIKACgDi/hP/yLd1/2ELj/ANDryMr/AIL/AMUvzPPwP8N+rO0r | |
| 230 | 1z0AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgDzb44aFc33h+21SzLi402QyE | |
| 231 | xnDBD1YH2IB/Ovm87oSnSjWhvD8n1+RwY2E5UuanvHUufCTx4/jDR3gvGB1Kzwsjf89FPR/r2P8A | |
| 232 | 9etspxzxVNwqfFH8V3/zHg8R9Yp3e63O9r3juCgDi/hP/wAi3df9hC4/9DryMr/gv/FL8zz8D/Df | |
| 233 | qztK9c9AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAbJGsqMjqHRgQysMgj0N | |
| 234 | JpSVmB5tpHw8n8E/EGHUNKDPot4HimiHJt8jI+q7gMHqOh9T8zSy+eCxsalHWDun5f8AAvbX7+74 | |
| 235 | qWGVGs5w2a1Xmel19OdoUAcX8J/+Rbuv+whcf+h15GV/wX/il+Z5+B/hv1Z2leuegFABQAUAFABQ | |
| 236 | AUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAHF/Cf/kW7r/sIXH/odeRlf8F/4pfm | |
| 237 | efgf4b9WdmTgV656AUALQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAMeVI3RWdVZ | |
| 238 | zhFJwWOM4HrwCfwqXJJpN7gPqgCgAoA4v4T/APIt3X/YQuP/AEOvIyv+C/8AFL8zz8D/AA36s6e/ | |
| 239 | nx+7H416M30O2T6FwdBWyLFoAhln8uWNO7Hn6VDlZ2FcmqxhQAUAFABQAUAFABQAUAFABQAUAFAB | |
| 240 | QAUAFABQAUAeafHW+udL0PSLy0dop4L9XWRf4SEevmc8lKFOnOLs1L9GcGOqSpUlOG6a/U6/wZ4m | |
| 241 | i8XeHbXUo8K7jbLGP4JB94f1+hFexgsSsXQVXr19TqpVFVgpx6m5XcahQBxPwpbZ4Zuz6X9z/wCh | |
| 242 | 14+WO1CX+KX5nn4H+G/Vm7kyyZPVjXdudW7Niuo2CgDLeTzLnd/tcVzN3dzK/vGrXSahQAUAFABQ | |
| 243 | AUAFABQAUAFABQAUAFABQAUAFABQAUAcb8WfD7+IfBN5HCpe4tyLiNR1JXqP++S1eNm1B18K+XeO | |
| 244 | v+f4HNiaXtqMoLc4b9nfVH83V9OJJTalwo9D91v/AGX8q8XIKr550ujV/wBP8jzcrm3CUH0f5/8A | |
| 245 | DHtdfZnthQBw3wx/5FO9/wCv+4/9Drxsu/3eX+J/medgv4T9WdNYxb5s9l5r0Yq7OyKNOug1Gv8A | |
| 246 | cb6UnsBlW6750HvXOldmMdzXrpNgoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAEoA5nQfAN | |
| 247 | h4b8Sahq1lmIXkYU2wHyoc5Yj2PHHbn6V5eHy+nhsRKtT2a27d/+GMYUYU5SnFay3Onr1DYKAOJ+ | |
| 248 | FK7/AAzdqe9/cj/x+vHyxXoS/wAUvzPPwX8N+rOyhiEKbR+frXrpWPQSsPzzTAKAK9ta+S7MfoPp | |
| 249 | WcY2YkrE7MEUknAFW3YYkUnmoGAwDQndXAfTAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg | |
| 250 | AoAQkKCScAdzRsBxXwkYP4ZuWUgqb+4II7jfXj5W06Da/mf5nn4L+G/VnbV7B6BTaYpf47HC1le0 | |
| 251 | ib62LlalCE4GTQBmXE5uJMD7vQCueT5jJu7saMaeXGq+grdKyNR9MAoAKACgAoAKACgAoAKACgAo | |
| 252 | AKACgAoAKACgAoAKAKupmMafcCWZrdGQp5sZIZSeBtxznJ4x3xWFdxVOXM7Lv117efbzHFNvQ8Xs | |
| 253 | PitqGiau2keLYZLmG3fYzooVz6M69HGOcDHXvxXxdPMqtOfscam0t+/z7+n56HlLHvD1XSrL5rt/ | |
| 254 | XY7v4QzJceFZpYzuR76dlPqC+RX0eVNSoNr+Z/maYF3pP1Z29ewegZt4uy6z64Nc8tzOW5oiug0K | |
| 255 | 9/Jshx/eOKzm9CZOyK9jBvfefujp7mogr6kxXU0a3NAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC | |
| 256 | gAoAKAI5547aF5ZWCRoMkmonONOLlJ6IaV3ZFS1t3upVu7pdrDPkwH/lkDxk/wC2R1PYHA7luenC | |
| 257 | VSXtai9F2/4L69tl1bpu3uo8++Mvw6fxDbDWNOi36hbriWJBzMg9PVh+o+grxM3y91l7ekveW67r | |
| 258 | /NfkeTjsL9YjzR+Jfj5D/gpcGDwXB/d86TI/GtcnlbDL1ZGXvlor1Z6YrBlBHQ19DuesRTwLNtJ4 | |
| 259 | KnrUyjcVrkm9f7w/OndDsMmgE+3J4HYd6TjzCauSKoUYAwB2qthjZJViXLHFJtINiq+ojPyoT9az | |
| 260 | dQjmRYgeSRcuoUdh3q4tvcpEtUMKACgAoAKACgAoAKACgAoAKACgAoAKACgDNh/4mt15x/49IHIi | |
| 261 | XtI44L/Qcge+T6GuKP8AtE+f7MdvN9/RbLz17Gj9xW6s0q7TMKAOD+F9os3hu5IO0i/uAPT75rxc | |
| 262 | tjei/wDE/wAzzcEr036s7W3RoY9jYJ7Yr1leOh6KViXZ68n3q7dxkV3J5UOR16VMnZCbsPUZUMvG | |
| 263 | RnFNd0MeDkVSdwKcllJM+5nH4dqycG9yXG5LDZpFz95vU1SgkNJIsVYwoAKACgAoAKACgAoAKACg | |
| 264 | AoAKACgAoAKAM/VpXKRWsTFZrltgZeqJ1ZvwHA9yK5MTJ2VKD1lp6Lq/u/Fo0gur6F2KJIIkjjUJ | |
| 265 | GgCqq9AB0FdMYqCUYqyRm3fVjbi6htULzSpCg/ikYKP1pSnGCvJ2CxmnxdoYk2HWLHd6faE/xrl+ | |
| 266 | u4W9vaR+9CutrmB8JGD+GbllIZTf3BBHQjfXJlbvQbX8z/M4MD/Dfqzs1PVia9Zdz0BjXcSn74/C | |
| 267 | lzoV0MuQLmD92d2D2pS95aA9UTouxFHoMVa0QxF4dvwNJbgPqgCgAoAKACgAoAKACgAoAKACgAoA | |
| 268 | KACgAoAKACgDNtf9K1q7m/htlFunsSA7n8coP+A1xU/3mInP+X3V+b/T7jR6RS76mlXaZmH4o8Ga | |
| 269 | T4vtxHqVsJGUYSZPlkT6H+h4rgxWBoYxfvVr36ozqU41Y8s1dHlurfs8TrITpmqxvH2S6Qgj8Vzn | |
| 270 | 8hXzVXIaif7qaa89PyuePPKoP4JW9dTq/g1YyW3hERmQKYbqaNtvchsV62UwccPbs2dOXx5aVvNn | |
| 271 | oCDKfnXuLY9Nkb2kbZ+QZpOCFZCCVIXjjVQA36Uk0rJBtoNlsvMYt5jfjScL9RNXH28HlZO5jn1p | |
| 272 | xjbUq1h08bSJhWKmqkrgzOdZoTzuHvmsHdGT5kSRX7p975x+tUptDUu5eimWZcqfwrZNMtO5JTGF | |
| 273 | ABQAUAFABQAUAFABQAUAFABQAUAZvh/59NE3e4kkmz6hmJH6YH4VxYPWlz/zNv73p+FjSp8VuxoS | |
| 274 | P5aMxBIUE4UZP5V2N2VzM4m5+M3ha0uWglup0kQlWDWsgKkdiCM14jzrCJ2bf3M5Z4qhTlyylZ/P | |
| 275 | /I2tC8c6F4kcR6fqUM0x58kkq/8A3ycGu2hj8NiXanPXts/xNoVYVPgkmZPwn/5Fu6/7CFx/6HXP | |
| 276 | lf8ABf8Ail+Zx4H+G/VnZ1656AtAFSeItdREd+9ZSXvCa1RZ2+pJrS3cYpOBk8CmAA5FABQA0xIf | |
| 277 | 4V/KlZAKqKnRQPoKEkgHUwCgAoAKACgAoAKACgAoAKACgAoAKAKWiJ5WjWCf3beMf+OiuXCrlw9N | |
| 278 | eS/IuesmXa6iDnvEvgLRPFgzqFkrTDpPGdkg/EdfxzXnYnL8Pitakde60f8AXqZVKNOsrVFc5GL4 | |
| 279 | A6JDdLMmoaiu1gwCugIPsdua8pZDRTvzy/D/ACOOOX0IS5o3+82PhIoi8LzrknF/cDLHJPz967sr | |
| 280 | 92g1/ef5jwP8N+rO26V7B6BGk6SOVU5x3qVJN2C5JVAAYEkdx1ouAjLuUj1GKTVwM2OZ7WQqegPI | |
| 281 | NYJuLMr8rsaSMHUMOhrdO5qOpgFABQAUAFABQAUAFABQAUAFABQAUAFABQBS0R/N0awf+9bxn/x0 | |
| 282 | Vy4V82HpvyX5Fz0ky7XUQFABQBxPwpGfDN2P+n+4/wDQ68fLdaEv8UvzPPwP8N+rOmvLrf8AIvA7 | |
| 283 | 16MpX2O2T6EthFsjLn+Lp9KqC6jirItVqUZ9rKTdn/azWEX7xCepo1uWVru184bl++P1rOUb6ktX | |
| 284 | I7CQgtG3BHODSg+go9i7WpYUAFABQAUAFABQAUAFABQAUAFABQAUAFAGb4f+TThB3t5Hhx6BWIH6 | |
| 285 | YP41xYPSlyfytr7np+FjSp8V+5pV2mYUAFAHDfC+TyvCt43f7fcY/wC+68bLXahL/FL8zzsE7Un6 | |
| 286 | s6e1tTK25+FHb1r0Yxudqj1ZogYFdBoBoAzbVf8ASwPQmueO5nFamnXQaBQAwxqXDY+Yd6VtbgPp | |
| 287 | gFABQAUAFABQAUAFABQAUAFABQAUAFABQBm23+i61dw/w3Ki4T3IAR/ywh/4FXFT/d4icP5veXy0 | |
| 288 | f6feaPWKfbQ0q7TMKACgDifhSgfw3c5Gcahcdf8Afrx8sV6L/wAUvzPPwX8N+rO06P8AUV62zPQH | |
| 289 | VQBQBBFBsuJH9elQo2dxW6k9WMKACgAoAKACgBCdoyego2AAdwBHQ0bgLQAUAFABQAUAFABQAUAF | |
| 290 | ABQB5pBoXirwlrkHkyya9oaSFk82QG4hQ8MvP3uOw9AeOlfKPD43BVYuF6kE++qXVa+Xy0vpsZUl | |
| 291 | UhNxbvF991/mvxPSIpUniSSNg8bgMrL0IPQ19TGSmlKLumbNW0Y+qEFAHF/Cf/kW7r/sIXH/AKHX | |
| 292 | kZX/AAX/AIpfmefgf4b9Wdk/3c+nNesz0BaYC0AFABQAUAFABQBXluDBMA3+rbv6Vm5crE3YnByM | |
| 293 | itBle+fbBj+8cVnN6Ey0RJbHMCfSqjsUiWqAKACgAoAKACgAoAKACgAoAKAM2H/iVXfkni0uGJiP | |
| 294 | aNzyU+h5I98j0FcUf9nnyfZlt5Pt89153XY0fvq/VGlXaZhQBxfwn/5Fu6/7CFx/6HXkZX/Bf+KX | |
| 295 | 5nn4H+G/VnaV656AxOMr6dPpUrTQY+qEFABQAUAFABQBFPCJ02nr2PpUyVxNXG2qPGhR+x4PtSim | |
| 296 | tGC0FuoTPHgdQaJK6Bq5Iq7FAHQVaVhkFxeCI7V+Z/5VnKdtiXKxJAH2ZkPzHt6VUb9SkS1QBQAU | |
| 297 | AFABQAUAFABQAUARXNsl3bvDICUcYODgj3B7H3rOpCNSLhLZjTs7or6dcyMZLa4IN1Bjc2MeYp+6 | |
| 298 | 4+uDn0INY0Zyd6dT4o/iuj+f53Kkuq2LtdRBxfwn/wCRbuv+whcf+h15GV/wX/il+Z5+B/hv1Z2l | |
| 299 | euegNYdx1FJgRzljFujOCOamW10DIIdQzxIMe4qVPuQpdy2rBxkHI9RWt7ljqACgAoATPNAATgZo | |
| 300 | egENtMZIskE8nms4y0BakpOV+UjPY1e60AghsxG25jvb1qIwtuSlYs1oUFABQAUAFABQAUAFABQA | |
| 301 | UAFAFHUbeQ+Xc24zcwZwv/PRT95PxwMe4FcteEtKlP4o/iuq+fTzsXF9HsWba4ju4EmiO6NxkGt4 | |
| 302 | TjUipx2ZLVnZnIfCf/kW7r/sIXH/AKHXl5X/AAX/AIpfmedgf4b9WdpXrnoBQA0jByPxHrSsBm3U | |
| 303 | HlvuX7h/T2rCSsZSVhsFw0DccjuKSdhJ2NSNxIgYdDXQnc2Bs7flxn3od+gFVkumP3gv0rO0mTqT | |
| 304 | xqyH5m3Njr0qldPUroMZ5i4/dDAPrSbk+gailSQUicIV6jFFuiAWFXQsGYN36U4prcBHvIozgtk+ | |
| 305 | 1DmkK6Q6O5jkOAwz6GmpJgnclqhhQAUAFABQAUAFABQAlAFK41zTrN9k9/awt/dkmVT+prnliaMH | |
| 306 | aU0vmg2C21vTrxtsF/bTN6RzKx/Q0QxNGekZp/NBuR/8gy/9LS6b8I5T/Rv/AEL/AHqy/gVf7svw | |
| 307 | l/wfz9TT4l5r8jnfhP8A8i3df9hC4/8AQ65cr/gv/FL8zy8D/DfqztK9c9AKACgBkkYcHjr1HrUt | |
| 308 | XAoS2TLkp8y+ncVi49jNx7D7ByjmNgRn19aqDs7DjpoX62LCgCJ5V8xUB+f2qG1ewXH7x34Poaq/ | |
| 309 | cCGJdk0rdd2MYqFo2FiZRjr1NWkA14UkPzKD70nFMLAsEadEA/ChRSAkqgCgAoAKACgAoAKACgD5 | |
| 310 | N1vx5r3iFm+26nOyH/llG2xP++RgV+W1sXXxH8Wbf5fdsfHVMdXqfat6aGCeTnvXHaxwtt7gpKsC | |
| 311 | CQR0I7UDUnHVM6LR/iBrmixNBHfST2rDBt7hi6/h3U+hBFdlPF1qcXCMnZ9On/A+Vj0aGY16LTvf | |
| 312 | 1PcPgrctd+DPOf78l1M5x0yWzX2+TycsNd73Z7mAfNRv5s76vbPRCgAoAKAGsMHI/EUn3AUc80wF | |
| 313 | oAr3k/kx8febpUTdiW7EVjARmRup6ZqYLqEVbVlmd/LiZvT1q5OyK2FicSRqw7007oNyveB4mEqE | |
| 314 | gdxWcrrVEyutUS21wJ19GHUVUZXGncmqxhQAUAFABQAUAFABQAUAfF9fkR+fhQAUAdN4H8A6h431 | |
| 315 | ARwKYbND++u2HyoPQere38q78HgquMnyw26vov8Ag+R6GFwcsQ7vSPf/ACPcfg7braeEpIEzsivZ | |
| 316 | 0XPXAbFfa5THkw7iujf5nv4BWpNLuzua9k9EKACgAoAKAGD5Wx2PSltoMfTEQvCpfzG+YjoDUNdQ | |
| 317 | sSqNqgVSVgI7ld8Dj2pSV0J6iWq7bdPpRHYFsSkZFUMgFr5UoeM49VrPls7oVrFitBhQAUAFABQA | |
| 318 | UAFABQAUAfP91+z7rsZ/cXtjMP8AaZ0/9lNfByyPFL4XF/N/5Hzzyl/Zn+A22/Z+1+Rv313YQj1D | |
| 319 | u3/sopRyPFPdpfN/5CWUy6zX3HW+H/gDpdi6y6pdyaiw58pB5ceffnJ/MV6lDIqcda8ubyWi/wA/ | |
| 320 | yO6ll1GnrL3n5/5Hp1lY2+m2sdvawx28EYwscahVH4V9LTpwpRUIKyR6m2hyXwn/AORbuv8AsIXH | |
| 321 | /odeZlf8F/4pfmefgf4b9WdpXrnoBQAUAFABQA1hkUmrgKp3DNCdwEblgPxpPsMdVCEoAOlAC0AF | |
| 322 | ABQAUAFABQAUAFABQAUAFABQAUAFABQBxfwn/wCRbuv+whcf+h15GV/wX/il+Z5+B/hv1Z2leueg | |
| 323 | FABQAUAFABQAwfK/1qdmAq8sT+FNdwHUwCgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA4v4 | |
| 324 | T/8AIt3X/YQuP/Q68jK/4L/xS/M8/A/w36s7SvXPQCgAoAjlnSEfMfwqXJIG7DLe6FwzADGPWlGX | |
| 325 | MJO5PVjGuPlyOo5pPYECjCihbAOpgFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQBxfwn/5F | |
| 326 | u6/7CFx/6HXkZX/Bf+KX5nn4H+G/VnZswUZJAHvXrt2PQK0l+i/dBY/pWbmuhLkkLNdGKFTgb2HT | |
| 327 | 0ocrIbdjOZi7ZJyT3rExbuXdOXCu3rxWtM0jsXa1LCgBkf3cenFJAPpgFABQAUAFABQAUAFABQAU | |
| 328 | AFABQAUAFABQAUAFAFHV9asdBs2utQuo7WBf4pD19gOpPsKwrV6eHjz1ZWQpSUVeTsji/hVqIfwr | |
| 329 | JJDykt5PIrEdi/FeTllS9BtdW/zPNwUv3Tt3Z1LyNIcsSfrXp3udjdwiXc4B6d/pQNK7FlkMshY9 | |
| 330 | 6G7ibuNRDIwVRkmjcErmtDEIYwo7V0pWRvsSUwCgBi8O350luwH0wCgAoAKACgAoAKACgAoAKACg | |
| 331 | AoAKACgAoAKAPP8A4gfFux8I+ZZ2gW+1UcGMH5Ij/tn19h+leBj82hhb06fvT/Bev+Rw4nGQwytv | |
| 332 | Lt/meA6/4k1HxPem61K6e4k7A8Kg9FHQCvh61epiJc9WV2fLV8RUxDvN/Loe2fBz/kSIP+u0n86+ | |
| 333 | 0yn/AHVerPewH8BfM7evYPRJFG2It/e+UUy1ohIoWmbCjPvQlcSVzSt7YQL6sepreMbGqViarGFA | |
| 334 | BQAwf6w/QVPUB9UAUAFABQAUAFABQAUAFABQAUAFABQAUAFAHkfxW+LR0xpdG0SX/Sxlbi7Q/wCq | |
| 335 | 9VX/AGvU9vr0+TzTNeS9DDvXq+3kvPz6eu3kY3G+x/d0/i/L/gnhjMXYsxLMTkk8kmvjj5dtyd2J | |
| 336 | QI9++Dn/ACJEH/XaT+dfdZT/ALqvVn0+A/gL5ncAFiAOpr2T0tzSFmpVA2SFHT3rbkNrEyqEGAAB | |
| 337 | 7VaVhjqYBQAUAFADP+Wn4UuoD6YBQAUAFABQAUAFABQAUAFABQAUAFABQB518X/iEfCumjT7GTbq | |
| 338 | l0p+ZesKdN31PQfia+dzbHvDx9jTfvP8F/m+h5+NxP1eGnxPb/M+dCSxJJJJ6k18IfINtu7CgQUA | |
| 339 | fQHwgTZ4It/eRz+tfd5UrYVerPqMB/AR6BYQbm8w9B0+te3BX1PUiupoVuaBQAUAFABQAUAMH+sP | |
| 340 | 0FT1AfVAFABQAUAFABQAUAFABQAUAFABQAUAFAHyB4i1248S61d6jck+ZO5bbnhV7KPYDivyitVl | |
| 341 | XqSqz3Z8RiKzr1HN/wBIzqxOYKACgD6J+EcBfwdZKO5Y199la/2aKPq8Ar0UeiogjQKOgr3krHqj | |
| 342 | qYBQAUAFABQAUAMX77fgKlbsB9UAUAFABQAUAFABQAUAFABQAUAFABQAUAfF9fkR+fhQAUAWNO06 | |
| 343 | 41a+hs7SJpriZtqRr1Jq4QlUkoQV2zWnTlVkoQ3Z9NfC+wbTfCkNvJjzYnaNivTI4r9Gy2Dp4dRe | |
| 344 | 6PrsHHkpcvY66vUO0KACgAoAKACgAoAan8X1qUA6qAKACgAoAKACgAoAKACgAoAKACgAoAKAPi+v | |
| 345 | yI/PwoA0dB8PX/ibUEs9Ot2uJm64+6g9WPYVtRo1K81Tpq7Omhh6mIlywR9G/D34bWfge18w7bnU | |
| 346 | 5FxLckfd/wBlPQfzr7/L8uhg1zPWb6/oj6zDYaGGjaO/Vmv4TTy9NlH/AE8SH/x6u3Cq0H6sugrR | |
| 347 | +bNuus6AoAKACgAoAKACgBqjaKSVgHUwCgAoAKACgAoAKACgAoAKACgAoAKACgD440/S7zVrgQWV | |
| 348 | rNdTH+CFCx/SvySEJVJcsFd+R8NToVK3wRuem+FPgNqF+Vm1uYafD18iMhpW/HoP1r6HC5JWq613 | |
| 349 | yr8f8kezQyvrWfyX+Z7PoHhvTvDFiLXTbVLaIdSOWc+rHqT9a+ww+GpYWPJSVv19T3YQjTXLBWRp | |
| 350 | 11FmZ4fj8uycf9NnP61z0FaPzZjSVomnXQbBQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAF | |
| 351 | ABQAUAFAEFpY29hEIraCK3jHRIkCj8hWcKcKS5YJJeQE9aAFABQBT0xPLt2H+2x/WsqasiIaIuVq | |
| 352 | WFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAEcKeWpHuTSSsJKxJT | |
| 353 | GFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAf/2Q==</one:Data></one:Image></one:Page> |
corpus/page-background/cold/read/page-003.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "MediumGrid - Microsoft OneNote", | |
| 9 | "title": "Standard - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-003.png| Binary files a/corpus/page-background/cold/read/page-003.png and b/corpus/page-background/cold/read/page-003.png differ |
corpus/page-background/cold/read/page-003.xml+352-1| ... | ... | @@ -1,2 +1,353 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5BFB88BD-0BC5-02DF-1E44-A227794FE0A1}{1}{B0}" name="MediumGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:20.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#E0C9FF" spacing="28.34639930725098"/><one:Vertical color="#E0C9FF" spacing="28.34639930725098"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{2E8AFDA7-8F2C-01A8-3BE9-691F5CC5678B}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[MediumGrid]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{569BE0FC-3A9E-0914-192E-92CEA56EE47E}{1}{B0}" name="Standard" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:21.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:19.000Z" objectID="{0F8A0709-8078-09B4-029D-DBEEB7DCBF25}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Standard]]></one:T></one:OE></one:Title><one:Image format="jpg" backgroundImage="true" lastModifiedTime="2026-09-29T17:12:32.000Z" objectID="{8A22F7F7-C573-08F0-2797-F47A607B1A73}{1}{B0}"><one:Position x="-27.0" y="-3.599999666213989" z="0"/><one:Size width="174.5" height="640.0" isSetByUser="true"/><one:Data>/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB | |
| 3 | EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL | |
| 4 | EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU | |
| 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID | |
| 6 | BAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHB | |
| 7 | FVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2 | |
| 8 | d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna | |
| 9 | 4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1 | |
| 10 | EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ0 | |
| 11 | 4SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeI | |
| 12 | iYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery | |
| 13 | 8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAOS8f+KbvwbFYamkX2nThL5V3EB8wDfdcH2wRjoc1 | |
| 14 | 5GYYqpg+SqleN7NfkzGtV9jDnauup0el6nbazp8F7aSCW2nUOjjuK9KlVhXgqkHdM1TUldbHK/Cf | |
| 15 | /kW7r/sIXH/odeblf8F/4pfmcGB/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAB | |
| 16 | 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</one:Data></one:Image></one:Page> |
corpus/page-background/cold/read/page-004.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "Wide - Microsoft OneNote", | |
| 9 | "title": "ColorRed - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-004.png| Binary files a/corpus/page-background/cold/read/page-004.png and b/corpus/page-background/cold/read/page-004.png differ |
corpus/page-background/cold/read/page-004.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5E86B14D-2EA5-0CEE-01DC-77C26A83BFD9}{1}{B0}" name="Wide" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:26.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="46.79999923706054"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:24.000Z" objectID="{CECBC798-72F2-0899-1721-97EFC8C976CA}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Wide]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{671FDF84-F889-0F7B-3E69-709FF1464096}{1}{B0}" name="ColorRed" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:04:15.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#FFD4D6" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{DB34D943-3109-03F2-203F-6BEA9A7CE879}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[ColorRed]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-005.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "Narrow - Microsoft OneNote", | |
| 9 | "title": "VeryLargeGrid - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-005.png| Binary files a/corpus/page-background/cold/read/page-005.png and b/corpus/page-background/cold/read/page-005.png differ |
corpus/page-background/cold/read/page-005.xml+769-1| ... | ... | @@ -1,2 +1,770 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{6A54865E-CC2F-025D-27BF-0929DFE646F9}{1}{B0}" name="Narrow" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:10.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:54:02.000Z" objectID="{606364C5-0652-06ED-22E6-A58C8D44710B}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Narrow]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{82A23C78-CA55-0FD2-00B0-0B6E28377DE9}{1}{B0}" name="VeryLargeGrid" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:57:47.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="#FDFDDD"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="56.69279861450195"/><one:Vertical color="#CAEBFD" spacing="56.69279861450195"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{F3F79A46-D617-05A2-10D6-DF5625D37B3E}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[VeryLargeGrid]]></one:T></one:OE></one:Title><one:Image format="jpg" backgroundImage="true" lastModifiedTime="2026-09-29T17:12:33.000Z" objectID="{8A22F7F7-C573-08F0-2797-F47A607B1A73}{7}{B0}"><one:Position x="-18.0" y="-3.599999666213989" z="0"/><one:Size width="611.4000244140625" height="791.2999877929687" isSetByUser="true"/><one:Data>/9j/4AAQSkZJRgABAgAAAQABAAD//gAbU25vd2JvdW5kIHRlbXBsYXRlIHNwYXJrc//AABEIAxgC | |
| 3 | ZAMBEQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4R | |
| 4 | DgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQU | |
| 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAA | |
| 6 | AQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgj | |
| 7 | QrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpz | |
| 8 | dHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX | |
| 9 | 2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/ | |
| 10 | xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEK | |
| 11 | FiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SF | |
| 12 | hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo | |
| 13 | 6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAPLNc/wCSlTf8B/8ARIr8OzL/AJKqfy/9No+4 | |
| 14 | w3/IqX9faNivpjzAoAKpAOXpTAWmgCqAVaCWLTQBTAKCWOXpVIQo60wH0AFUgLEC/aIjH/GnzL7j | |
| 15 | uK66cfawcOq1X6oyk+R36EaJv3ewzWUI81/ItuxJbw+YxZv9Wgyx/pWlKnzu8tluTOXLot2Ru5kc | |
| 16 | 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</one:Data></one:Image></one:Page> |
corpus/page-background/cold/read/page-006.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "Standard - Microsoft OneNote", | |
| 9 | "title": "MediumGrid - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-006.png| Binary files a/corpus/page-background/cold/read/page-006.png and b/corpus/page-background/cold/read/page-006.png differ |
corpus/page-background/cold/read/page-006.xml+1-1| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{859BC3C1-DA73-0094-187D-081E0BF4B451}{1}{B0}" name="Standard" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:21.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:19.000Z" objectID="{DC8A2434-6095-0034-03CE-413E1946EF0A}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Standard]]></one:T></one:OE></one:Title><one:Image format="jpg" backgroundImage="true" lastModifiedTime="2026-09-29T08:39:38.000Z" objectID="{48C362B0-6114-0C90-07D0-9F6A4AEBE04B}{1}{B0}"><one:Position x="-27.0" y="-3.599999666213989" z="0"/><one:Size width="174.5" height="640.0" isSetByUser="true"/><one:Data>/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{88FBAB80-EB28-0B5F-1F17-38F7D7D5B08E}{1}{B0}" name="MediumGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:20.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#E0C9FF" spacing="28.34639930725098"/><one:Vertical color="#E0C9FF" spacing="28.34639930725098"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{FD8ADE9A-6FC1-0828-3ABA-F3CFF25F37A4}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[MediumGrid]]></one:T></one:OE></one:Title><one:Image format="jpg" backgroundImage="true" lastModifiedTime="2026-09-29T17:12:33.000Z" objectID="{8A22F7F7-C573-08F0-2797-F47A607B1A73}{4}{B0}"><one:Position x="-27.0" y="-3.599999666213989" z="0"/><one:Size width="174.5" height="640.0" isSetByUser="true"/><one:Data>/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB | |
| 3 | 3 | EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL |
| 4 | 4 | EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU |
| 5 | 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID |
corpus/page-background/cold/read/page-007.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "LargeGrid - Microsoft OneNote", | |
| 9 | "title": "Wide - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-007.png| Binary files a/corpus/page-background/cold/read/page-007.png and b/corpus/page-background/cold/read/page-007.png differ |
corpus/page-background/cold/read/page-007.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{9E24A66D-E1BF-0540-29D9-FD682782DED2}{1}{B0}" name="LargeGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:42.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="42.51959991455078"/><one:Vertical color="#CAEBFD" spacing="42.51959991455078"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{1915C34E-9F05-0F41-0FB3-CA265D7BA96F}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[LargeGrid]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{8D869270-CE48-056E-008F-ED12C419EFF6}{1}{B0}" name="Wide" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:26.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="46.79999923706054"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:24.000Z" objectID="{1DCBE4A5-921F-0119-1672-0D3F665326E5}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Wide]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-008.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "ColorRed - Microsoft OneNote", | |
| 9 | "title": "Narrow - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-008.png| Binary files a/corpus/page-background/cold/read/page-008.png and b/corpus/page-background/cold/read/page-008.png differ |
corpus/page-background/cold/read/page-008.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{B41FFCB9-1864-06FB-3F3A-EA4F5FDC10B9}{1}{B0}" name="ColorRed" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:04:15.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#FFD4D6" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T07:53:43.000Z" objectID="{0834FA7E-D1E4-0A72-216C-F13A34E6B856}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[ColorRed]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{B954A563-2CC2-0BDD-26EC-93F9717C16D6}{1}{B0}" name="Narrow" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T08:04:10.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:54:02.000Z" objectID="{B36347F8-E6BF-0F6D-23B5-3F5C23DE2124}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Narrow]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-009.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "SmallGrid - Microsoft OneNote", | |
| 9 | "title": "None - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-009.png| Binary files a/corpus/page-background/cold/read/page-009.png and b/corpus/page-background/cold/read/page-009.png differ |
corpus/page-background/cold/read/page-009.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D343E8F3-DEB6-0F1E-1EB1-8B94E7382757}{1}{B0}" name="SmallGrid" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:31.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="#E8D8C8"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="12.0"/><one:Vertical color="#CAEBFD" spacing="12.0"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{4D4C9C73-4470-097D-2E98-6D8AACCDD845}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[SmallGrid]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{E2D01146-745A-02DE-3F2E-FDCFFA4F0CC3}{1}{B0}" name="None" dateTime="2026-09-29T07:53:40.000Z" lastModifiedTime="2026-09-29T17:14:47.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:53:42.000Z" objectID="{7C1744E1-1D2B-023C-0E92-ABBE3C4AD3B0}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[None]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/read/page-010.navigation.json+1-1| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | "h": 600, |
| 7 | 7 | "error": null, |
| 8 | 8 | "win": { |
| 9 | "title": "Hidden - Microsoft OneNote", | |
| 9 | "title": "College - Microsoft OneNote", | |
| 10 | 10 | "class": "Framework::CFrame", |
| 11 | 11 | "dialog": false |
| 12 | 12 | } |
corpus/page-background/cold/read/page-010.png| Binary files a/corpus/page-background/cold/read/page-010.png and b/corpus/page-background/cold/read/page-010.png differ |
corpus/page-background/cold/read/page-010.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{ED27E4E9-4094-0D2D-34A2-4C38706F92CD}{1}{B0}" name="Hidden" dateTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:06:00.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="#D4F9F2"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#FFFFFF" spacing="33.11999893188476"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:43.000Z" lastModifiedTime="2026-09-29T08:05:45.000Z" objectID="{25B3BEA8-E4BC-026D-1015-2BBF16614A26}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Hidden]]></one:T></one:OE></one:Title></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{E4C9122E-5D75-0421-0745-41D679F1CDC8}{1}{B0}" name="College" dateTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:16.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="true"><one:Horizontal color="#CAEBFD" spacing="23.76000022888184"/><one:Margin color="#FF5050"/></one:RuleLines></one:PageSettings><one:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-29T07:53:42.000Z" lastModifiedTime="2026-09-29T07:57:06.000Z" objectID="{28696816-7DD6-02EE-1482-718B337330F7}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[College]]></one:T></one:OE></one:Title></one:Page> |
corpus/page-background/cold/run.json+1-1| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | { |
| 2 | "notebook": "/private/tmp/snowbound-page-background/candidate", | |
| 2 | "notebook": "/private/tmp/snowbound-art-undo/candidate", | |
| 3 | 3 | "expected_pages": 11, |
| 4 | 4 | "author": null, |
| 5 | 5 | "author_timeout_seconds": 600, |
corpus/page-background/cold/source.json+3-3| ... | ... | @@ -7,8 +7,8 @@ |
| 7 | 7 | }, |
| 8 | 8 | { |
| 9 | 9 | "path": "Rules.one", |
| 10 | "bytes": 132704, | |
| 11 | "sha256": "a513e56274ae794cf558da34dca7189225159d7eb6ab7d2256531fd7a2a55b44", | |
| 12 | "mtime_ns": 1790671178905073494 | |
| 10 | "bytes": 324016, | |
| 11 | "sha256": "53c49a8bd35b4aee2ec840ef79fd7bdb6e6b4bc3cb3885d56cccbac145389a8b", | |
| 12 | "mtime_ns": 1790701954004992686 | |
| 13 | 13 | } |
| 14 | 14 | ] |
crates/canvas/assets/file.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#eef1f5"/></linearGradient> | |
| 4 | <linearGradient id="g1" x2="0" y2="1"><stop offset="0" stop-color="#f4f6f9"/><stop offset="1" stop-color="#d3dae3"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.5 2.25L10 2.25L13.5 5.75L13.5 15.25L3.5 15.25Z" fill="#000000" fill-opacity="0.12"/> | |
| 7 | <path d="M3.5 1.5L10 1.5L13.5 5L13.5 14.5L3.5 14.5Z" fill="url(#g0)" stroke="#a3acb8" stroke-width="0.8" stroke-linejoin="round"/> | |
| 8 | <path d="M10 1.5L10 5L13.5 5Z" fill="url(#g1)" stroke="#a3acb8" stroke-width="0.8" stroke-linejoin="round"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-down-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 15.2L1.4 8.4L5.4 8.4L5.4 0.8L10.6 0.8L10.6 8.4L14.6 8.4Z" fill="#3d62ad"/> | |
| 7 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-down-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 15.2L1.4 8.4L5.4 8.4L5.4 0.8L10.6 0.8L10.6 8.4L14.6 8.4Z" fill="#3f8a2c"/> | |
| 7 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-down-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 15.2L1.4 8.4L5.4 8.4L5.4 0.8L10.6 0.8L10.6 8.4L14.6 8.4Z" fill="#b0801a"/> | |
| 7 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 13.8L3.1 8.9L6 8.9L6 1.5L10 1.5L10 8.9L12.9 8.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-left-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8L7.6 1.4L7.6 5.4L15.2 5.4L15.2 10.6L7.6 10.6L7.6 14.6Z" fill="#3d62ad"/> | |
| 7 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-left-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8L7.6 1.4L7.6 5.4L15.2 5.4L15.2 10.6L7.6 10.6L7.6 14.6Z" fill="#3f8a2c"/> | |
| 7 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-left-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8L7.6 1.4L7.6 5.4L15.2 5.4L15.2 10.6L7.6 10.6L7.6 14.6Z" fill="#b0801a"/> | |
| 7 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g0)"/> | |
| 8 | <path d="M2.2 8L7.1 3.1L7.1 6L14.5 6L14.5 10L7.1 10L7.1 12.9Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-right-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M15.2 8L8.4 14.6L8.4 10.6L0.8 10.6L0.8 5.4L8.4 5.4L8.4 1.4Z" fill="#3d62ad"/> | |
| 7 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-right-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M15.2 8L8.4 14.6L8.4 10.6L0.8 10.6L0.8 5.4L8.4 5.4L8.4 1.4Z" fill="#3f8a2c"/> | |
| 7 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-right-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M15.2 8L8.4 14.6L8.4 10.6L0.8 10.6L0.8 5.4L8.4 5.4L8.4 1.4Z" fill="#b0801a"/> | |
| 7 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M13.8 8L8.9 12.9L8.9 10L1.5 10L1.5 6L8.9 6L8.9 3.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-up-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 0.8L14.6 7.6L10.6 7.6L10.6 15.2L5.4 15.2L5.4 7.6L1.4 7.6Z" fill="#3d62ad"/> | |
| 7 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-up-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 0.8L14.6 7.6L10.6 7.6L10.6 15.2L5.4 15.2L5.4 7.6L1.4 7.6Z" fill="#3f8a2c"/> | |
| 7 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/arrow-up-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M8 0.8L14.6 7.6L10.6 7.6L10.6 15.2L5.4 15.2L5.4 7.6L1.4 7.6Z" fill="#b0801a"/> | |
| 7 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 2.2L12.9 7.1L10 7.1L10 14.5L6 14.5L6 7.1L3.1 7.1Z" fill="url(#g1)" fill-opacity="0.35"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/bell.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2.4" x2="0" y2="12.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff0a0"/><stop offset="1" stop-color="#d8a010"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 2.5L4 5" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 6 | <path d="M15 2.5L12 5" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 7 | <path d="M0.6 10L3.4 10" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 8 | <path d="M15.4 10L12.6 10" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 9 | <path d="M2 15L4.4 13" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 10 | <path d="M14 15L11.6 13" fill="none" stroke="#f0c020" stroke-width="0.7"/> | |
| 11 | <path d="M8 2.4C10.4 2.4 11.2 4.4 11.4 6.8C11.6 9.2 12.4 10.4 13.6 11.2V12.4H2.4V11.2C3.6 10.4 4.4 9.2 4.6 6.8C4.8 4.4 5.6 2.4 8 2.4Z" fill="url(#g0)"/> | |
| 12 | <path d="M8 2.4C10.4 2.4 11.2 4.4 11.4 6.8C11.6 9.2 12.4 10.4 13.6 11.2V12.4H2.4V11.2C3.6 10.4 4.4 9.2 4.6 6.8C4.8 4.4 5.6 2.4 8 2.4Z" fill="none" stroke="#a87810" stroke-width="0.5"/> | |
| 13 | <path d="M6.8 13.4A1.2 1.2 0 1 1 9.2 13.4A1.2 1.2 0 1 1 6.8 13.4Z" fill="#a87810"/> | |
| 14 | <path d="M7.2 1.8A0.8 0.8 0 1 1 8.8 1.8A0.8 0.8 0 1 1 7.2 1.8Z" fill="#a87810"/> | |
| 15 | </svg> |
crates/canvas/assets/tags/binoculars.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="5.6" x2="0" y2="14.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d4dcf0"/><stop offset="1" stop-color="#6a78b0"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1.4" x2="0" y2="6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e4eaf8"/><stop offset="1" stop-color="#7a88bc"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="6.2" x2="0" y2="10" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b4c0e0"/><stop offset="1" stop-color="#6a78b0"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M2.2 5.6H5.8A1.2 1.2 0 0 1 7 6.8V13.4A1.2 1.2 0 0 1 5.8 14.6H2.2A1.2 1.2 0 0 1 1 13.4V6.8A1.2 1.2 0 0 1 2.2 5.6Z" fill="url(#g0)"/> | |
| 8 | <path d="M2.8 1.4H5.2A0.8 0.8 0 0 1 6 2.2V5.2A0.8 0.8 0 0 1 5.2 6H2.8A0.8 0.8 0 0 1 2 5.2V2.2A0.8 0.8 0 0 1 2.8 1.4Z" fill="url(#g1)"/> | |
| 9 | <path d="M1.6 12.4H6.4V14H1.6Z" fill="#2a3470" fill-opacity="0.6"/> | |
| 10 | <path d="M10.2 5.6H13.8A1.2 1.2 0 0 1 15 6.8V13.4A1.2 1.2 0 0 1 13.8 14.6H10.2A1.2 1.2 0 0 1 9 13.4V6.8A1.2 1.2 0 0 1 10.2 5.6Z" fill="url(#g0)"/> | |
| 11 | <path d="M10.8 1.4H13.2A0.8 0.8 0 0 1 14 2.2V5.2A0.8 0.8 0 0 1 13.2 6H10.8A0.8 0.8 0 0 1 10 5.2V2.2A0.8 0.8 0 0 1 10.8 1.4Z" fill="url(#g1)"/> | |
| 12 | <path d="M9.6 12.4H14.4V14H9.6Z" fill="#2a3470" fill-opacity="0.6"/> | |
| 13 | <path d="M6.6 6.2H9.4V10H6.6Z" fill="url(#g2)"/> | |
| 14 | </svg> |
crates/canvas/assets/tags/calendar.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e4eaf4"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1.5" x2="0" y2="4.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8fb1ec"/><stop offset="1" stop-color="#4d78c8"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M1 1.5H15V15H1Z" fill="#7d8fb0"/> | |
| 7 | <path d="M1.6 2.1H14.4V14.4H1.6Z" fill="url(#g0)"/> | |
| 8 | <path d="M1.6 2.1H14.4V4.5H1.6Z" fill="url(#g1)"/> | |
| 9 | <path d="M2.4 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 10 | <path d="M4.64 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 11 | <path d="M6.88 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 12 | <path d="M9.12 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 13 | <path d="M11.36 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 14 | <path d="M13.6 0.9V2.7" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 15 | <path d="M1.8 6.65H14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 16 | <path d="M1.8 8.8H14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 17 | <path d="M1.8 10.96H14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 18 | <path d="M1.8 13.11H14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 19 | <path d="M3.33 4.5V14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 20 | <path d="M5.67 4.5V14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 21 | <path d="M8 4.5V14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 22 | <path d="M10.33 4.5V14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 23 | <path d="M12.67 4.5V14.2" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 24 | <path d="M9.6 6.6H12V8.9H9.6Z" fill="none" stroke="#e0442c" stroke-width="0.8"/> | |
| 25 | </svg> |
crates/canvas/assets/tags/car.svg created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="3" x2="0" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e04040"/><stop offset="1" stop-color="#8a0a0a"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="4.2" x2="0" y2="7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e0ecff"/><stop offset="1" stop-color="#8aa4d0"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M1 10.4C1 8.4 2 7.6 3 7.2L4.4 4.2C4.8 3.4 5.4 3 6.4 3H9.6C10.6 3 11.2 3.4 11.6 4.2L13 7.2C14 7.6 15 8.4 15 10.4V12H1Z" fill="url(#g0)"/> | |
| 7 | <path d="M5.2 4.2H10.8L11.8 7H4.2Z" fill="url(#g1)"/> | |
| 8 | <path d="M1.4 11.6H3.8V14H1.4Z" fill="#2a2a2a"/> | |
| 9 | <path d="M12.2 11.6H14.6V14H12.2Z" fill="#2a2a2a"/> | |
| 10 | <path d="M2 9.2A1.4 0.9 0 1 1 4.8 9.2A1.4 0.9 0 1 1 2 9.2Z" fill="#fff8d0"/> | |
| 11 | <path d="M11.2 9.2A1.4 0.9 0 1 1 14 9.2A1.4 0.9 0 1 1 11.2 9.2Z" fill="#fff8d0"/> | |
| 12 | <path d="M5.6 10.2H10.4V11.2H5.6Z" fill="#c0c4cc"/> | |
| 13 | </svg> |
crates/canvas/assets/tags/checkbox-green.svg created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#a6d596"/> | |
| 5 | <stop offset="1" stop-color="#6cb055"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#ffffff"/> | |
| 9 | <stop offset="1" stop-color="#ffffff"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#98cf84"/> | |
| 13 | <stop offset="1" stop-color="#5ea347"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="layer3" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 16 | <stop offset="0" stop-color="#ffffff"/> | |
| 17 | <stop offset="1" stop-color="#e2f3da"/> | |
| 18 | </linearGradient> | |
| 19 | </defs> | |
| 20 | <path d="M0 0H16V16H0Z" fill="url(#layer0)"/> | |
| 21 | <path d="M1 1H15V15H1Z" fill="url(#layer1)"/> | |
| 22 | <path d="M2 2H14V14H2Z" fill="url(#layer2)"/> | |
| 23 | <path d="M3 3H13V13H3Z" fill="url(#layer3)"/> | |
| 24 | </svg> |
crates/canvas/assets/tags/checkbox-yellow.svg created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#f6d993"/> | |
| 5 | <stop offset="1" stop-color="#e0ad3e"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#ffffff"/> | |
| 9 | <stop offset="1" stop-color="#ffffff"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#f3d182"/> | |
| 13 | <stop offset="1" stop-color="#d9a133"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="layer3" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 16 | <stop offset="0" stop-color="#ffffff"/> | |
| 17 | <stop offset="1" stop-color="#fcf1d8"/> | |
| 18 | </linearGradient> | |
| 19 | </defs> | |
| 20 | <path d="M0 0H16V16H0Z" fill="url(#layer0)"/> | |
| 21 | <path d="M1 1H15V15H1Z" fill="url(#layer1)"/> | |
| 22 | <path d="M2 2H14V14H2Z" fill="url(#layer2)"/> | |
| 23 | <path d="M3 3H13V13H3Z" fill="url(#layer3)"/> | |
| 24 | </svg> |
crates/canvas/assets/tags/circle-1-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0c8f2"/><stop offset="1" stop-color="#5c86d1"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3d62ad"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.6 5.6L8.4 4.5V11.6" fill="none" stroke="#1b2c55" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-1-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c0e6ae"/><stop offset="1" stop-color="#6cb450"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3f8a2c"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.6 5.6L8.4 4.5V11.6" fill="none" stroke="#1d4414" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-1-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fae397"/><stop offset="1" stop-color="#e6b433"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#b0801a"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.6 5.6L8.4 4.5V11.6" fill="none" stroke="#4a3408" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-2-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0c8f2"/><stop offset="1" stop-color="#5c86d1"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3d62ad"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 6.1C6.3 4.9 7.2 4.4 8.1 4.4C9.2 4.4 10 5.1 10 6.1C10 7.2 9.1 8 6.2 11.4H10.2" fill="none" stroke="#1b2c55" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-2-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c0e6ae"/><stop offset="1" stop-color="#6cb450"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3f8a2c"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 6.1C6.3 4.9 7.2 4.4 8.1 4.4C9.2 4.4 10 5.1 10 6.1C10 7.2 9.1 8 6.2 11.4H10.2" fill="none" stroke="#1d4414" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-2-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fae397"/><stop offset="1" stop-color="#e6b433"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#b0801a"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 6.1C6.3 4.9 7.2 4.4 8.1 4.4C9.2 4.4 10 5.1 10 6.1C10 7.2 9.1 8 6.2 11.4H10.2" fill="none" stroke="#4a3408" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-3-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0c8f2"/><stop offset="1" stop-color="#5c86d1"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3d62ad"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 5.2C6.6 4.6 7.3 4.4 8 4.4C9.1 4.4 9.9 5 9.9 5.9C9.9 6.9 9.1 7.6 7.7 7.6C9.3 7.6 10.2 8.4 10.2 9.6C10.2 10.8 9.2 11.6 8 11.6C7.1 11.6 6.3 11.3 5.8 10.6" fill="none" stroke="#1b2c55" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-3-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c0e6ae"/><stop offset="1" stop-color="#6cb450"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3f8a2c"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 5.2C6.6 4.6 7.3 4.4 8 4.4C9.1 4.4 9.9 5 9.9 5.9C9.9 6.9 9.1 7.6 7.7 7.6C9.3 7.6 10.2 8.4 10.2 9.6C10.2 10.8 9.2 11.6 8 11.6C7.1 11.6 6.3 11.3 5.8 10.6" fill="none" stroke="#1d4414" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-3-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fae397"/><stop offset="1" stop-color="#e6b433"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#b0801a"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M6.1 5.2C6.6 4.6 7.3 4.4 8 4.4C9.1 4.4 9.9 5 9.9 5.9C9.9 6.9 9.1 7.6 7.7 7.6C9.3 7.6 10.2 8.4 10.2 9.6C10.2 10.8 9.2 11.6 8 11.6C7.1 11.6 6.3 11.3 5.8 10.6" fill="none" stroke="#4a3408" stroke-width="1.15"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/circle-blue.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0c8f2"/><stop offset="1" stop-color="#5c86d1"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3d62ad"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/circle-green.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c0e6ae"/><stop offset="1" stop-color="#6cb450"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#3f8a2c"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/circle-yellow.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fae397"/><stop offset="1" stop-color="#e6b433"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="#b0801a"/> | |
| 6 | <path d="M1.7 8A6.3 6.3 0 1 1 14.3 8A6.3 6.3 0 1 1 1.7 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.66 5.34A4.34 2.8 0 1 1 12.34 5.34A4.34 2.8 0 1 1 3.66 5.34Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/clock.svg created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8ccd4"/><stop offset="1" stop-color="#7a808c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf2"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M1 8A7 7 0 1 1 15 8A7 7 0 1 1 1 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M1.9 8A6.1 6.1 0 1 1 14.1 8A6.1 6.1 0 1 1 1.9 8Z" fill="url(#g1)"/> | |
| 8 | <path d="M12.9 8A0.5 0.5 0 1 1 13.9 8A0.5 0.5 0 1 1 12.9 8Z" fill="#4a5060"/> | |
| 9 | <path d="M12.33 10.7A0.35 0.35 0 1 1 13.03 10.7A0.35 0.35 0 1 1 12.33 10.7Z" fill="#4a5060"/> | |
| 10 | <path d="M10.35 12.68A0.35 0.35 0 1 1 11.05 12.68A0.35 0.35 0 1 1 10.35 12.68Z" fill="#4a5060"/> | |
| 11 | <path d="M7.5 13.4A0.5 0.5 0 1 1 8.5 13.4A0.5 0.5 0 1 1 7.5 13.4Z" fill="#4a5060"/> | |
| 12 | <path d="M4.95 12.68A0.35 0.35 0 1 1 5.65 12.68A0.35 0.35 0 1 1 4.95 12.68Z" fill="#4a5060"/> | |
| 13 | <path d="M2.97 10.7A0.35 0.35 0 1 1 3.67 10.7A0.35 0.35 0 1 1 2.97 10.7Z" fill="#4a5060"/> | |
| 14 | <path d="M2.1 8A0.5 0.5 0 1 1 3.1 8A0.5 0.5 0 1 1 2.1 8Z" fill="#4a5060"/> | |
| 15 | <path d="M2.97 5.3A0.35 0.35 0 1 1 3.67 5.3A0.35 0.35 0 1 1 2.97 5.3Z" fill="#4a5060"/> | |
| 16 | <path d="M4.95 3.32A0.35 0.35 0 1 1 5.65 3.32A0.35 0.35 0 1 1 4.95 3.32Z" fill="#4a5060"/> | |
| 17 | <path d="M7.5 2.6A0.5 0.5 0 1 1 8.5 2.6A0.5 0.5 0 1 1 7.5 2.6Z" fill="#4a5060"/> | |
| 18 | <path d="M10.35 3.32A0.35 0.35 0 1 1 11.05 3.32A0.35 0.35 0 1 1 10.35 3.32Z" fill="#4a5060"/> | |
| 19 | <path d="M12.33 5.3A0.35 0.35 0 1 1 13.03 5.3A0.35 0.35 0 1 1 12.33 5.3Z" fill="#4a5060"/> | |
| 20 | <path d="M8 3.8V8L10.94 6.25" fill="none" stroke="#20242c" stroke-width="0.84"/> | |
| 21 | </svg> |
crates/canvas/assets/tags/cloud.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2" x2="0" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#b4c8ec"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 11.2C1.6 11.2 0.6 10 0.8 8.6C1 7.2 2.2 6.4 3.4 6.6C3.4 4.4 5.2 2.8 7.2 3C8.4 1.6 10.8 1.6 12 3C13.8 3.2 15 4.6 14.8 6.4C15.6 7.2 15.6 8.8 14.8 9.8C14 10.8 12.8 11.2 11.6 11C10.6 12 8.8 12.2 7.6 11.4C6.4 12 4.6 12 3.4 11.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M3.4 11.2C1.6 11.2 0.6 10 0.8 8.6C1 7.2 2.2 6.4 3.4 6.6C3.4 4.4 5.2 2.8 7.2 3C8.4 1.6 10.8 1.6 12 3C13.8 3.2 15 4.6 14.8 6.4C15.6 7.2 15.6 8.8 14.8 9.8C14 10.8 12.8 11.2 11.6 11C10.6 12 8.8 12.2 7.6 11.4C6.4 12 4.6 12 3.4 11.2Z" fill="none" stroke="#7a94c4" stroke-width="0.5"/> | |
| 7 | <path d="M3.5 13.2A0.9 0.9 0 1 1 5.3 13.2A0.9 0.9 0 1 1 3.5 13.2Z" fill="#9ab0dc"/> | |
| 8 | <path d="M1.8 15A0.6 0.6 0 1 1 3 15A0.6 0.6 0 1 1 1.8 15Z" fill="#9ab0dc"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/coins.svg created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e4eaf4"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="3.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8fb1ec"/><stop offset="1" stop-color="#4d78c8"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="13" x2="0" y2="14.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe070"/><stop offset="1" stop-color="#d09a18"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="11.4" x2="0" y2="13.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe070"/><stop offset="1" stop-color="#d09a18"/></linearGradient> | |
| 7 | <linearGradient id="g4" x1="0" y1="9.8" x2="0" y2="11.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe070"/><stop offset="1" stop-color="#d09a18"/></linearGradient> | |
| 8 | <linearGradient id="g5" x1="0" y1="8.2" x2="0" y2="10" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe070"/><stop offset="1" stop-color="#d09a18"/></linearGradient> | |
| 9 | <linearGradient id="g6" x1="0" y1="6.6" x2="0" y2="8.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe070"/><stop offset="1" stop-color="#d09a18"/></linearGradient> | |
| 10 | </defs> | |
| 11 | <path d="M0.6 1H11.6V12H0.6Z" fill="#7a8aac"/> | |
| 12 | <path d="M1.1 1.5H11.1V11.5H1.1Z" fill="url(#g0)"/> | |
| 13 | <path d="M1.1 1.5H11.1V3.2H1.1Z" fill="url(#g1)"/> | |
| 14 | <path d="M2.2 4.1H3.2V5.1H2.2Z" fill="#6a7a9a"/> | |
| 15 | <path d="M4.2 4.6H9.6" fill="none" stroke="#9aa8c4" stroke-width="0.6"/> | |
| 16 | <path d="M2.2 6.5H3.2V7.5H2.2Z" fill="#6a7a9a"/> | |
| 17 | <path d="M4.2 7H9.6" fill="none" stroke="#9aa8c4" stroke-width="0.6"/> | |
| 18 | <path d="M2.2 8.9H3.2V9.9H2.2Z" fill="#6a7a9a"/> | |
| 19 | <path d="M4.2 9.4H9.6" fill="none" stroke="#9aa8c4" stroke-width="0.6"/> | |
| 20 | <path d="M8.2 14.2A3.6 1.2 0 1 1 15.4 14.2A3.6 1.2 0 1 1 8.2 14.2Z" fill="#8a6010"/> | |
| 21 | <path d="M8.5 13.9A3.3 0.9 0 1 1 15.1 13.9A3.3 0.9 0 1 1 8.5 13.9Z" fill="url(#g2)"/> | |
| 22 | <path d="M8.2 12.6A3.6 1.2 0 1 1 15.4 12.6A3.6 1.2 0 1 1 8.2 12.6Z" fill="#8a6010"/> | |
| 23 | <path d="M8.5 12.3A3.3 0.9 0 1 1 15.1 12.3A3.3 0.9 0 1 1 8.5 12.3Z" fill="url(#g3)"/> | |
| 24 | <path d="M8.2 11A3.6 1.2 0 1 1 15.4 11A3.6 1.2 0 1 1 8.2 11Z" fill="#8a6010"/> | |
| 25 | <path d="M8.5 10.7A3.3 0.9 0 1 1 15.1 10.7A3.3 0.9 0 1 1 8.5 10.7Z" fill="url(#g4)"/> | |
| 26 | <path d="M8.2 9.4A3.6 1.2 0 1 1 15.4 9.4A3.6 1.2 0 1 1 8.2 9.4Z" fill="#8a6010"/> | |
| 27 | <path d="M8.5 9.1A3.3 0.9 0 1 1 15.1 9.1A3.3 0.9 0 1 1 8.5 9.1Z" fill="url(#g5)"/> | |
| 28 | <path d="M8.2 7.8A3.6 1.2 0 1 1 15.4 7.8A3.6 1.2 0 1 1 8.2 7.8Z" fill="#8a6010"/> | |
| 29 | <path d="M8.5 7.5A3.3 0.9 0 1 1 15.1 7.5A3.3 0.9 0 1 1 8.5 7.5Z" fill="url(#g6)"/> | |
| 30 | </svg> |
crates/canvas/assets/tags/date.svg created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.2" x2="0" y2="13.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e4eaf4"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1.2" x2="0" y2="4.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8fb1ec"/><stop offset="1" stop-color="#4d78c8"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="7.4" x2="0" y2="15.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8ccd4"/><stop offset="1" stop-color="#7a808c"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="7.4" x2="0" y2="15.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf2"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <path d="M1 1.2H13.8V13.4H1Z" fill="#7d8fb0"/> | |
| 9 | <path d="M1.6 1.8H13.2V12.8H1.6Z" fill="url(#g0)"/> | |
| 10 | <path d="M1.6 1.8H13.2V4.2H1.6Z" fill="url(#g1)"/> | |
| 11 | <path d="M2.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 12 | <path d="M4.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 13 | <path d="M6.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 14 | <path d="M8.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 15 | <path d="M10.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 16 | <path d="M12.4 0.6V2.4" fill="none" stroke="#6c7890" stroke-width="0.7"/> | |
| 17 | <path d="M1.8 6.07H13" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 18 | <path d="M1.8 7.94H13" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 19 | <path d="M1.8 9.81H13" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 20 | <path d="M1.8 11.68H13" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 21 | <path d="M3.13 4.2V12.6" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 22 | <path d="M5.27 4.2V12.6" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 23 | <path d="M7.4 4.2V12.6" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 24 | <path d="M9.53 4.2V12.6" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 25 | <path d="M11.67 4.2V12.6" fill="none" stroke="#b4c2dc" stroke-width="0.5"/> | |
| 26 | <path d="M3.8 5H9V11.8H3.8Z" fill="#ffffff" fill-opacity="0.85"/> | |
| 27 | <path d="M8.2 5.4H5.2L4.8 8C5.4 7.4 6.2 7.2 6.8 7.4C8.2 7.8 8.4 9.8 7.6 10.8C6.8 11.8 5 11.6 4.4 10.8" fill="none" stroke="#3a4050" stroke-width="1.1"/> | |
| 28 | <path d="M7.4 11.6A4.2 4.2 0 1 1 15.8 11.6A4.2 4.2 0 1 1 7.4 11.6Z" fill="url(#g2)"/> | |
| 29 | <path d="M8.3 11.6A3.3 3.3 0 1 1 14.9 11.6A3.3 3.3 0 1 1 8.3 11.6Z" fill="url(#g3)"/> | |
| 30 | <path d="M13.7 11.6A0.5 0.5 0 1 1 14.7 11.6A0.5 0.5 0 1 1 13.7 11.6Z" fill="#4a5060"/> | |
| 31 | <path d="M13.5 12.9A0.35 0.35 0 1 1 14.2 12.9A0.35 0.35 0 1 1 13.5 12.9Z" fill="#4a5060"/> | |
| 32 | <path d="M12.55 13.85A0.35 0.35 0 1 1 13.25 13.85A0.35 0.35 0 1 1 12.55 13.85Z" fill="#4a5060"/> | |
| 33 | <path d="M11.1 14.2A0.5 0.5 0 1 1 12.1 14.2A0.5 0.5 0 1 1 11.1 14.2Z" fill="#4a5060"/> | |
| 34 | <path d="M9.95 13.85A0.35 0.35 0 1 1 10.65 13.85A0.35 0.35 0 1 1 9.95 13.85Z" fill="#4a5060"/> | |
| 35 | <path d="M9 12.9A0.35 0.35 0 1 1 9.7 12.9A0.35 0.35 0 1 1 9 12.9Z" fill="#4a5060"/> | |
| 36 | <path d="M8.5 11.6A0.5 0.5 0 1 1 9.5 11.6A0.5 0.5 0 1 1 8.5 11.6Z" fill="#4a5060"/> | |
| 37 | <path d="M9 10.3A0.35 0.35 0 1 1 9.7 10.3A0.35 0.35 0 1 1 9 10.3Z" fill="#4a5060"/> | |
| 38 | <path d="M9.95 9.35A0.35 0.35 0 1 1 10.65 9.35A0.35 0.35 0 1 1 9.95 9.35Z" fill="#4a5060"/> | |
| 39 | <path d="M11.1 9A0.5 0.5 0 1 1 12.1 9A0.5 0.5 0 1 1 11.1 9Z" fill="#4a5060"/> | |
| 40 | <path d="M12.55 9.35A0.35 0.35 0 1 1 13.25 9.35A0.35 0.35 0 1 1 12.55 9.35Z" fill="#4a5060"/> | |
| 41 | <path d="M13.5 10.3A0.35 0.35 0 1 1 14.2 10.3A0.35 0.35 0 1 1 13.5 10.3Z" fill="#4a5060"/> | |
| 42 | <path d="M11.6 9.08V11.6L13.36 10.55" fill="none" stroke="#20242c" stroke-width="0.6"/> | |
| 43 | </svg> |
crates/canvas/assets/tags/dollar.svg created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M8 0.8V15.2" fill="none" stroke="#3a8a3a" stroke-width="1.2"/> | |
| 3 | <path d="M11.6 4.2C11 2.8 9.6 2.2 8 2.2C5.8 2.2 4.4 3.2 4.4 4.8C4.4 8.2 11.8 7.2 11.8 11C11.8 12.8 10.2 13.8 8 13.8C6.2 13.8 4.8 13 4.2 11.6" fill="none" stroke="#3a8a3a" stroke-width="2.2"/> | |
| 4 | <path d="M11.6 4.2C11 2.8 9.6 2.2 8 2.2C5.8 2.2 4.4 3.2 4.4 4.8C4.4 8.2 11.8 7.2 11.8 11C11.8 12.8 10.2 13.8 8 13.8C6.2 13.8 4.8 13 4.2 11.6" fill="none" stroke="#8ad87a" stroke-width="0.8"/> | |
| 5 | </svg> |
crates/canvas/assets/tags/envelope-open.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2.9" x2="0" y2="13.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f4f4ee"/><stop offset="1" stop-color="#dcdcd0"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="7.4" x2="0" y2="13.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf4"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M1 7.2L8 2.2L15 7.2V14.2H1Z" fill="#8a96a8"/> | |
| 7 | <path d="M1.6 7.4L8 2.9L14.4 7.4V13.6H1.6Z" fill="url(#g0)"/> | |
| 8 | <path d="M3 5H13V11H3Z" fill="#ffffff"/> | |
| 9 | <path d="M1.6 7.4L8 11.6L14.4 7.4V13.6H1.6Z" fill="url(#g1)"/> | |
| 10 | <path d="M1.6 13.6L6.6 9.8M14.4 13.6L9.4 9.8" fill="none" stroke="#aab4c4" stroke-width="0.5"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/envelope.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4" x2="0" y2="12.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff4c0"/><stop offset="1" stop-color="#f6dc88"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 3.4H15.2V12.8H0.8Z" fill="#c0a050"/> | |
| 6 | <path d="M1.4 4H14.6V12.2H1.4Z" fill="url(#g0)"/> | |
| 7 | <path d="M1.4 4L8 9L14.6 4M1.4 12.2L6.4 7.8M14.6 12.2L9.6 7.8" fill="none" stroke="#c8a860" stroke-width="0.6"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/flag-next-week.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fbcbc7"/><stop offset="1" stop-color="#ecb8b5"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/flag-no-date.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fde1df"/><stop offset="1" stop-color="#f4d6d5"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/flag-this-week.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fab4ae"/><stop offset="1" stop-color="#e49a96"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/flag-today.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f46a5e"/><stop offset="1" stop-color="#c8342c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/flag-tomorrow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7978e"/><stop offset="1" stop-color="#d8716b"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/follow-up.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#7aa4f0"/><stop offset="1" stop-color="#2450b0"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M3.4 2.2C6.4 0.2 9 3.6 14.6 1.6C13.3 3.8 13.7 5.3 15.4 6.8C10.4 8.8 7.8 5.4 5 7.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.8 1.6L7.4 15" fill="none" stroke="#2a3a6a" stroke-width="1.3"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/frown.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff9a6a"/><stop offset="1" stop-color="#d8401a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.6 8A7.4 7.4 0 1 1 15.4 8A7.4 7.4 0 1 1 0.6 8Z" fill="#b83010"/> | |
| 6 | <path d="M1.2 8A6.8 6.8 0 1 1 14.8 8A6.8 6.8 0 1 1 1.2 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.8 4.6A4.2 2.6 0 1 1 12.2 4.6A4.2 2.6 0 1 1 3.8 4.6Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M5 6.8A0.8 1.1 0 1 1 6.6 6.8A0.8 1.1 0 1 1 5 6.8Z" fill="#2a2008"/> | |
| 9 | <path d="M9.4 6.8A0.8 1.1 0 1 1 11 6.8A0.8 1.1 0 1 1 9.4 6.8Z" fill="#2a2008"/> | |
| 10 | <path d="M4.8 12.2C6.4 10.4 9.6 10.4 11.2 12.2" fill="none" stroke="#2a2008" stroke-width="0.9"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/globe.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.2" x2="0" y2="14.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6aa4ff"/><stop offset="1" stop-color="#1e54b8"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="2.6" x2="0" y2="11.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ad86a"/><stop offset="1" stop-color="#2e8a2a"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="8.6" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ad86a"/><stop offset="1" stop-color="#2e8a2a"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M0.6 8A7.4 7.4 0 1 1 15.4 8A7.4 7.4 0 1 1 0.6 8Z" fill="#1e4a9a"/> | |
| 8 | <path d="M1.2 8A6.8 6.8 0 1 1 14.8 8A6.8 6.8 0 1 1 1.2 8Z" fill="url(#g0)"/> | |
| 9 | <path d="M3 5C4.4 3 6.6 2.4 8 3C7.6 4 8.6 4.6 9.6 4.4C10.4 5.4 9.4 6.6 8 6.6C7.2 7.6 7.8 8.8 6.6 9.4C5.2 10 4.6 11.6 3.4 11.2C2.2 9.4 1.8 7 3 5Z" fill="url(#g1)"/> | |
| 10 | <path d="M10.4 9C12 8.6 13.8 9.4 14.2 10.6C13.4 12.6 11.8 14 10 14.4C9.4 13 10.2 12 9.6 10.8C9.4 10 9.8 9.2 10.4 9Z" fill="url(#g2)"/> | |
| 11 | <path d="M3.2 3.8A3.8 2 0 1 1 10.8 3.8A3.8 2 0 1 1 3.2 3.8Z" fill="#ffffff" fill-opacity="0.35"/> | |
| 12 | </svg> |
crates/canvas/assets/tags/green-square.svg created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#7ea25a"/> | |
| 5 | <stop offset="1" stop-color="#6a8e48"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#b0d88c"/> | |
| 9 | <stop offset="1" stop-color="#b4dc92"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#8cb568"/> | |
| 13 | <stop offset="1" stop-color="#86ae62"/> | |
| 14 | </linearGradient> | |
| 15 | </defs> | |
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | |
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | |
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | |
| 19 | </svg> |
crates/canvas/assets/tags/heart.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2" x2="0" y2="13.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff6a6a"/><stop offset="1" stop-color="#c80a0a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 15C5.6 12.6 0.8 9.6 0.8 5.4C0.8 2.8 2.6 1.2 4.6 1.2C6.2 1.2 7.4 2.2 8 3.4C8.6 2.2 9.8 1.2 11.4 1.2C13.4 1.2 15.2 2.8 15.2 5.4C15.2 9.6 10.4 12.6 8 15Z" fill="#6a0808"/> | |
| 6 | <path d="M8 13.8C5.8 11.6 1.6 9 1.6 5.4C1.6 3.4 3 2 4.6 2C6.2 2 7.4 3.2 8 4.8C8.6 3.2 9.8 2 11.4 2C13 2 14.4 3.4 14.4 5.4C14.4 9 10.2 11.6 8 13.8Z" fill="url(#g0)"/> | |
| 7 | <path d="M2.8 4.6A1.8 1.2 0 1 1 6.4 4.6A1.8 1.2 0 1 1 2.8 4.6Z" fill="#ffffff" fill-opacity="0.5"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/im-contact.svg created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="8.4" x2="0" y2="15.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ae07a"/><stop offset="1" stop-color="#1e8a3a"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="0.8" x2="0" y2="8.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9ae88a"/><stop offset="1" stop-color="#2a9a2a"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M1 14.6C1.4 10.6 4.4 8.4 8 8.4C11.6 8.4 14.6 10.6 15 14.6C12.8 15.4 10.4 15.6 8 15.6C5.6 15.6 3.2 15.4 1 14.6Z" fill="url(#g0)"/> | |
| 7 | <path d="M8 9.2C10 9.2 12.2 10 13.4 11.6C12 11 10 10.6 8 10.6Z" fill="#1e5ab8" fill-opacity="0.9"/> | |
| 8 | <path d="M4.6 4.6A3.8 3.8 0 1 1 12.2 4.6A3.8 3.8 0 1 1 4.6 4.6Z" fill="url(#g1)"/> | |
| 9 | <path d="M5.6 3.2A2 1.2 0 1 1 9.6 3.2A2 1.2 0 1 1 5.6 3.2Z" fill="#ffffff" fill-opacity="0.5"/> | |
| 10 | </svg> |
crates/canvas/assets/tags/key.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.8" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff0a0"/><stop offset="1" stop-color="#c89414"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M5.4 0.8H10.6V6.2H9V13.2L7.8 15H7.4L6.4 13.6L7.2 12.6L6.4 11.8L7.2 11L6.4 10.2L7 9.4V6.2H5.4Z" fill="url(#g0)"/> | |
| 6 | <path d="M5.4 0.8H10.6V6.2H9V13.2L7.8 15H7.4L6.4 13.6L7.2 12.6L6.4 11.8L7.2 11L6.4 10.2L7 9.4V6.2H5.4Z" fill="none" stroke="#9a7010" stroke-width="0.5"/> | |
| 7 | <path d="M7 2.2H9V3H7Z" fill="#9a7010"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/laptop.svg created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2.4" x2="0" y2="9.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a8c8ff"/><stop offset="1" stop-color="#3a6ad8"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="8.6" x2="0" y2="14.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e0e4f0"/><stop offset="1" stop-color="#8a90b0"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.4 1.6L13.6 3.4L12.4 10.4L2.2 8.6Z" fill="#5a6070"/> | |
| 7 | <path d="M4 2.4L12.8 4L11.8 9.6L3 8Z" fill="url(#g0)"/> | |
| 8 | <path d="M2.2 8.6L12.4 10.4L15.2 12.4L4.6 14.6L0.8 11.2Z" fill="url(#g1)"/> | |
| 9 | <path d="M3 10.4L11.8 11.8L9.2 12.8L3.6 11.8Z" fill="#9aa2c0"/> | |
| 10 | </svg> |
crates/canvas/assets/tags/lightning.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.8" x2="0" y2="15.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff4a0"/><stop offset="1" stop-color="#e0a818"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 0.8H14L10.2 6.2H13.2L4.8 15.2L7 8.6H3.6Z" fill="url(#g0)"/> | |
| 6 | <path d="M8 0.8H14L10.2 6.2H13.2L4.8 15.2L7 8.6H3.6Z" fill="none" stroke="#b88410" stroke-width="0.5"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-arrow-blue.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6fb0ff"/><stop offset="1" stop-color="#1e5ab8"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 2H4.5V0L9 3.75L4.5 7.5V5.5H3V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/mark-arrow-green.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ee07a"/><stop offset="1" stop-color="#2e8a1e"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 2H4.5V0L9 3.75L4.5 7.5V5.5H3V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/mark-arrow-yellow.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe07a"/><stop offset="1" stop-color="#c28a12"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 2H4.5V0L9 3.75L4.5 7.5V5.5H3V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/mark-arrow.svg deleted-6| ... | ... | @@ -1,6 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6fb0ff"/><stop offset="1" stop-color="#1e5ab8"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 2H4.5V0L9 3.75L4.5 7.5V5.5H3V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/mark-client.svg deleted-9| ... | ... | @@ -1,9 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff1b0"/><stop offset="1" stop-color="#e0a82e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b58a3a"/><stop offset="1" stop-color="#8a6420"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.5 0.25C5.2 0.25 5.8 1.3 5.4 3L4.4 7.25H2.6L1.6 3C1.2 1.3 1.8 0.25 3.5 0.25Z" fill="url(#layer1)"/> | |
| 7 | <path d="M3.5 1C4.6 1 4.9 1.7 4.6 3L3.8 6.5H3.2L2.4 3C2.1 1.7 2.4 1 3.5 1Z" fill="url(#layer0)"/> | |
| 8 | <path d="M3.5 8A1 1 0 1 1 3.5 10A1 1 0 1 1 3.5 8Z" fill="url(#layer1)"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-exclamation-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8e6ff"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#3d62ad"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.5 0.25C5.2 0.25 5.8 1.3 5.4 3L4.4 7.25H2.6L1.6 3C1.2 1.3 1.8 0.25 3.5 0.25Z" fill="url(#layer1)"/> | |
| 7 | <path d="M3.5 1C4.6 1 4.9 1.7 4.6 3L3.8 6.5H3.2L2.4 3C2.1 1.7 2.4 1 3.5 1Z" fill="url(#layer0)"/> | |
| 8 | <path d="M3.5 8A1 1 0 1 1 3.5 10A1 1 0 1 1 3.5 8Z" fill="url(#layer1)"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-exclamation-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8f5c8"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4a8a2e"/><stop offset="1" stop-color="#2d6a1a"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.5 0.25C5.2 0.25 5.8 1.3 5.4 3L4.4 7.25H2.6L1.6 3C1.2 1.3 1.8 0.25 3.5 0.25Z" fill="url(#layer1)"/> | |
| 7 | <path d="M3.5 1C4.6 1 4.9 1.7 4.6 3L3.8 6.5H3.2L2.4 3C2.1 1.7 2.4 1 3.5 1Z" fill="url(#layer0)"/> | |
| 8 | <path d="M3.5 8A1 1 0 1 1 3.5 10A1 1 0 1 1 3.5 8Z" fill="url(#layer1)"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-exclamation-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff1b0"/><stop offset="1" stop-color="#e0a82e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b58a3a"/><stop offset="1" stop-color="#8a6420"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M3.5 0.25C5.2 0.25 5.8 1.3 5.4 3L4.4 7.25H2.6L1.6 3C1.2 1.3 1.8 0.25 3.5 0.25Z" fill="url(#layer1)"/> | |
| 7 | <path d="M3.5 1C4.6 1 4.9 1.7 4.6 3L3.8 6.5H3.2L2.4 3C2.1 1.7 2.4 1 3.5 1Z" fill="url(#layer0)"/> | |
| 8 | <path d="M3.5 8A1 1 0 1 1 3.5 10A1 1 0 1 1 3.5 8Z" fill="url(#layer1)"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-flag-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="5.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6f9ae8"/><stop offset="1" stop-color="#2450b0"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.4 1.2L5.4 11.6" fill="none" stroke="#2a3a6a" stroke-width="1"/> | |
| 6 | <path d="M1.5 1.4C3.5 0.4 5.2 2.2 8.8 1.2C8 2.8 8.4 3.6 9.6 4.6C6.4 5.6 4.9 4.1 2.8 5Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-flag-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="5.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#7fe07a"/><stop offset="1" stop-color="#2a9a25"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.4 1.2L5.4 11.6" fill="none" stroke="#2a3a6a" stroke-width="1"/> | |
| 6 | <path d="M1.5 1.4C3.5 0.4 5.2 2.2 8.8 1.2C8 2.8 8.4 3.6 9.6 4.6C6.4 5.6 4.9 4.1 2.8 5Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-flag-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.5" x2="0" y2="5.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd27a"/><stop offset="1" stop-color="#e08a1a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.4 1.2L5.4 11.6" fill="none" stroke="#2a3a6a" stroke-width="1"/> | |
| 6 | <path d="M1.5 1.4C3.5 0.4 5.2 2.2 8.8 1.2C8 2.8 8.4 3.6 9.6 4.6C6.4 5.6 4.9 4.1 2.8 5Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-manager.svg deleted-9| ... | ... | @@ -1,9 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa2ae"/><stop offset="1" stop-color="#4a525e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f5d45c"/><stop offset="1" stop-color="#b8901e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M4 0.5A2.5 2.5 0 1 1 4 5.5A2.5 2.5 0 1 1 4 0.5Z" fill="url(#layer0)"/> | |
| 7 | <path d="M4 5.5C6.4 5.5 7.75 6.5 7.75 8.5V10H0.25V8.5C0.25 6.5 1.6 5.5 4 5.5Z" fill="url(#layer1)"/> | |
| 8 | <path d="M3.4 5.75H4.6L4 7.25Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-one-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5a8ad8"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M3.25 2.1L4.5 1.6V7.25H3.25V3.3L2.3 3.7V2.6Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-one-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62b556"/><stop offset="1" stop-color="#237a1c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M3.25 2.1L4.5 1.6V7.25H3.25V3.3L2.3 3.7V2.6Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-one-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8b84a"/><stop offset="1" stop-color="#a8781a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M3.25 2.1L4.5 1.6V7.25H3.25V3.3L2.3 3.7V2.6Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-one.svg deleted-7| ... | ... | @@ -1,7 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5a8ad8"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M3.25 2.1L4.5 1.6V7.25H3.25V3.3L2.3 3.7V2.6Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-person-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa2ae"/><stop offset="1" stop-color="#4a525e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6f9ae0"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M4 0.5A2.5 2.5 0 1 1 4 5.5A2.5 2.5 0 1 1 4 0.5Z" fill="url(#layer0)"/> | |
| 7 | <path d="M4 5.5C6.4 5.5 7.75 6.5 7.75 8.5V10H0.25V8.5C0.25 6.5 1.6 5.5 4 5.5Z" fill="url(#layer1)"/> | |
| 8 | <path d="M3.4 5.75H4.6L4 7.25Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-person-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa2ae"/><stop offset="1" stop-color="#4a525e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#86d070"/><stop offset="1" stop-color="#2e7a1e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M4 0.5A2.5 2.5 0 1 1 4 5.5A2.5 2.5 0 1 1 4 0.5Z" fill="url(#layer0)"/> | |
| 7 | <path d="M4 5.5C6.4 5.5 7.75 6.5 7.75 8.5V10H0.25V8.5C0.25 6.5 1.6 5.5 4 5.5Z" fill="url(#layer1)"/> | |
| 8 | <path d="M3.4 5.75H4.6L4 7.25Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-person-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa2ae"/><stop offset="1" stop-color="#4a525e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f5d45c"/><stop offset="1" stop-color="#b8901e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M4 0.5A2.5 2.5 0 1 1 4 5.5A2.5 2.5 0 1 1 4 0.5Z" fill="url(#layer0)"/> | |
| 7 | <path d="M4 5.5C6.4 5.5 7.75 6.5 7.75 8.5V10H0.25V8.5C0.25 6.5 1.6 5.5 4 5.5Z" fill="url(#layer1)"/> | |
| 8 | <path d="M3.4 5.75H4.6L4 7.25Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-person.svg deleted-9| ... | ... | @@ -1,9 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa2ae"/><stop offset="1" stop-color="#4a525e"/></linearGradient> | |
| 4 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6f9ae0"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M4 0.5A2.5 2.5 0 1 1 4 5.5A2.5 2.5 0 1 1 4 0.5Z" fill="url(#layer0)"/> | |
| 7 | <path d="M4 5.5C6.4 5.5 7.75 6.5 7.75 8.5V10H0.25V8.5C0.25 6.5 1.6 5.5 4 5.5Z" fill="url(#layer1)"/> | |
| 8 | <path d="M3.4 5.75H4.6L4 7.25Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/mark-star-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M4.4 0.1L5.72 2.78L8.68 3.21L6.54 5.3L7.05 8.24L4.4 6.85L1.75 8.24L2.26 5.3L0.12 3.21L3.08 2.78Z" fill="#3d62ad"/> | |
| 6 | <path d="M4.4 1.2L5.43 3.28L7.73 3.62L6.06 5.24L6.46 7.53L4.4 6.45L2.34 7.53L2.74 5.24L1.07 3.62L3.37 3.28Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-star-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M4.4 0.1L5.72 2.78L8.68 3.21L6.54 5.3L7.05 8.24L4.4 6.85L1.75 8.24L2.26 5.3L0.12 3.21L3.08 2.78Z" fill="#3f8a2c"/> | |
| 6 | <path d="M4.4 1.2L5.43 3.28L7.73 3.62L6.06 5.24L6.46 7.53L4.4 6.45L2.34 7.53L2.74 5.24L1.07 3.62L3.37 3.28Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-star-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcf0c0"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M4.4 0.1L5.72 2.78L8.68 3.21L6.54 5.3L7.05 8.24L4.4 6.85L1.75 8.24L2.26 5.3L0.12 3.21L3.08 2.78Z" fill="#b0801a"/> | |
| 6 | <path d="M4.4 1.2L5.43 3.28L7.73 3.62L6.06 5.24L6.46 7.53L4.4 6.45L2.34 7.53L2.74 5.24L1.07 3.62L3.37 3.28Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-three-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5a8ad8"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.3 2.4C2.6 1.8 3.1 1.5 3.8 1.5C4.7 1.5 5.3 2 5.3 2.7C5.3 3.5 4.6 4 3.7 4C4.8 4 5.5 4.6 5.5 5.5C5.5 6.4 4.8 7.1 3.8 7.1C3.1 7.1 2.5 6.8 2.1 6.2" fill="none" stroke="#ffffff" stroke-width="1.1"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-three-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62b556"/><stop offset="1" stop-color="#237a1c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.3 2.4C2.6 1.8 3.1 1.5 3.8 1.5C4.7 1.5 5.3 2 5.3 2.7C5.3 3.5 4.6 4 3.7 4C4.8 4 5.5 4.6 5.5 5.5C5.5 6.4 4.8 7.1 3.8 7.1C3.1 7.1 2.5 6.8 2.1 6.2" fill="none" stroke="#ffffff" stroke-width="1.1"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-three-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8b84a"/><stop offset="1" stop-color="#a8781a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#g0)"/> | |
| 6 | <path d="M2.3 2.4C2.6 1.8 3.1 1.5 3.8 1.5C4.7 1.5 5.3 2 5.3 2.7C5.3 3.5 4.6 4 3.7 4C4.8 4 5.5 4.6 5.5 5.5C5.5 6.4 4.8 7.1 3.8 7.1C3.1 7.1 2.5 6.8 2.1 6.2" fill="none" stroke="#ffffff" stroke-width="1.1"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-two-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#5a8ad8"/><stop offset="1" stop-color="#1e4494"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M2 2.9C2.1 1.9 2.8 1.5 3.75 1.5C4.9 1.5 5.5 2.1 5.5 3C5.5 3.9 4.9 4.5 3.6 5.7L3.3 6H5.6V7.25H1.9V6.2L3.3 4.9C4.1 4.1 4.25 3.7 4.25 3.2C4.25 2.8 4 2.6 3.7 2.6C3.35 2.6 3.2 2.85 3.15 3.2Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-two-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62b556"/><stop offset="1" stop-color="#237a1c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M2 2.9C2.1 1.9 2.8 1.5 3.75 1.5C4.9 1.5 5.5 2.1 5.5 3C5.5 3.9 4.9 4.5 3.6 5.7L3.3 6H5.6V7.25H1.9V6.2L3.3 4.9C4.1 4.1 4.25 3.7 4.25 3.2C4.25 2.8 4 2.6 3.7 2.6C3.35 2.6 3.2 2.85 3.15 3.2Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-two-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8b84a"/><stop offset="1" stop-color="#a8781a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M2 2.9C2.1 1.9 2.8 1.5 3.75 1.5C4.9 1.5 5.5 2.1 5.5 3C5.5 3.9 4.9 4.5 3.6 5.7L3.3 6H5.6V7.25H1.9V6.2L3.3 4.9C4.1 4.1 4.25 3.7 4.25 3.2C4.25 2.8 4 2.6 3.7 2.6C3.35 2.6 3.2 2.85 3.15 3.2Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mark-two.svg deleted-7| ... | ... | @@ -1,7 +0,0 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8b84a"/><stop offset="1" stop-color="#a8781a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.5 0.5H7V8.5H0.5Z" fill="url(#layer0)"/> | |
| 6 | <path d="M2 2.9C2.1 1.9 2.8 1.5 3.75 1.5C4.9 1.5 5.5 2.1 5.5 3C5.5 3.9 4.9 4.5 3.6 5.7L3.3 6H5.6V7.25H1.9V6.2L3.3 4.9C4.1 4.1 4.25 3.7 4.25 3.2C4.25 2.8 4 2.6 3.7 2.6C3.35 2.6 3.2 2.85 3.15 3.2Z" fill="#ffffff"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/mobile.svg created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.6" x2="0" y2="15.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9aa4b4"/><stop offset="1" stop-color="#5a6474"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1.2" x2="0" y2="14.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f0f2f6"/><stop offset="1" stop-color="#c4cad4"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="2.2" x2="0" y2="7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ab4ff"/><stop offset="1" stop-color="#2e62d0"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M5.4 0.6H10.6A1.4 1.4 0 0 1 12 2V14A1.4 1.4 0 0 1 10.6 15.4H5.4A1.4 1.4 0 0 1 4 14V2A1.4 1.4 0 0 1 5.4 0.6Z" fill="url(#g0)"/> | |
| 8 | <path d="M5.6 1.2H10.4A1 1 0 0 1 11.4 2.2V13.8A1 1 0 0 1 10.4 14.8H5.6A1 1 0 0 1 4.6 13.8V2.2A1 1 0 0 1 5.6 1.2Z" fill="url(#g1)"/> | |
| 9 | <path d="M5.4 2.2H10.6V7H5.4Z" fill="url(#g2)"/> | |
| 10 | <path d="M5.6 8.4H6.8V9.4H5.6Z" fill="#7a8494"/> | |
| 11 | <path d="M7.4 8.4H8.6V9.4H7.4Z" fill="#7a8494"/> | |
| 12 | <path d="M9.2 8.4H10.4V9.4H9.2Z" fill="#7a8494"/> | |
| 13 | <path d="M5.6 10.3H6.8V11.3H5.6Z" fill="#7a8494"/> | |
| 14 | <path d="M7.4 10.3H8.6V11.3H7.4Z" fill="#7a8494"/> | |
| 15 | <path d="M9.2 10.3H10.4V11.3H9.2Z" fill="#7a8494"/> | |
| 16 | <path d="M5.6 12.2H6.8V13.2H5.6Z" fill="#7a8494"/> | |
| 17 | <path d="M7.4 12.2H8.6V13.2H7.4Z" fill="#7a8494"/> | |
| 18 | <path d="M9.2 12.2H10.4V13.2H9.2Z" fill="#7a8494"/> | |
| 19 | </svg> |
crates/canvas/assets/tags/notebook.svg created+32| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.4" x2="0" y2="14.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9ad88a"/><stop offset="1" stop-color="#4a9a4a"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="6.6" x2="0" y2="15.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8ccd4"/><stop offset="1" stop-color="#7a808c"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="6.6" x2="0" y2="15.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf2"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M3.4 0.8H15.2V15.2H3.4Z" fill="#3a7a3a"/> | |
| 8 | <path d="M4 1.4H14.6V14.6H4Z" fill="url(#g0)"/> | |
| 9 | <path d="M6.4 3H12.8V7.2H6.4Z" fill="#3a7a3a" fill-opacity="0.35"/> | |
| 10 | <path d="M2.8 2H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 11 | <path d="M2.8 4H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 12 | <path d="M2.8 6H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 13 | <path d="M2.8 8H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 14 | <path d="M2.8 10H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 15 | <path d="M2.8 12H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 16 | <path d="M2.8 14H4.8" fill="none" stroke="#4a4a4a" stroke-width="0.7"/> | |
| 17 | <path d="M0.6 11A4.4 4.4 0 1 1 9.4 11A4.4 4.4 0 1 1 0.6 11Z" fill="url(#g1)"/> | |
| 18 | <path d="M1.5 11A3.5 3.5 0 1 1 8.5 11A3.5 3.5 0 1 1 1.5 11Z" fill="url(#g2)"/> | |
| 19 | <path d="M7.3 11A0.5 0.5 0 1 1 8.3 11A0.5 0.5 0 1 1 7.3 11Z" fill="#4a5060"/> | |
| 20 | <path d="M7.07 12.4A0.35 0.35 0 1 1 7.77 12.4A0.35 0.35 0 1 1 7.07 12.4Z" fill="#4a5060"/> | |
| 21 | <path d="M6.05 13.42A0.35 0.35 0 1 1 6.75 13.42A0.35 0.35 0 1 1 6.05 13.42Z" fill="#4a5060"/> | |
| 22 | <path d="M4.5 13.8A0.5 0.5 0 1 1 5.5 13.8A0.5 0.5 0 1 1 4.5 13.8Z" fill="#4a5060"/> | |
| 23 | <path d="M3.25 13.42A0.35 0.35 0 1 1 3.95 13.42A0.35 0.35 0 1 1 3.25 13.42Z" fill="#4a5060"/> | |
| 24 | <path d="M2.23 12.4A0.35 0.35 0 1 1 2.93 12.4A0.35 0.35 0 1 1 2.23 12.4Z" fill="#4a5060"/> | |
| 25 | <path d="M1.7 11A0.5 0.5 0 1 1 2.7 11A0.5 0.5 0 1 1 1.7 11Z" fill="#4a5060"/> | |
| 26 | <path d="M2.23 9.6A0.35 0.35 0 1 1 2.93 9.6A0.35 0.35 0 1 1 2.23 9.6Z" fill="#4a5060"/> | |
| 27 | <path d="M3.25 8.58A0.35 0.35 0 1 1 3.95 8.58A0.35 0.35 0 1 1 3.25 8.58Z" fill="#4a5060"/> | |
| 28 | <path d="M4.5 8.2A0.5 0.5 0 1 1 5.5 8.2A0.5 0.5 0 1 1 4.5 8.2Z" fill="#4a5060"/> | |
| 29 | <path d="M6.05 8.58A0.35 0.35 0 1 1 6.75 8.58A0.35 0.35 0 1 1 6.05 8.58Z" fill="#4a5060"/> | |
| 30 | <path d="M7.07 9.6A0.35 0.35 0 1 1 7.77 9.6A0.35 0.35 0 1 1 7.07 9.6Z" fill="#4a5060"/> | |
| 31 | <path d="M5 8.36V11L6.85 9.9" fill="none" stroke="#20242c" stroke-width="0.6"/> | |
| 32 | </svg> |
crates/canvas/assets/tags/orange-square.svg created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#e07a4e"/> | |
| 5 | <stop offset="1" stop-color="#c4643a"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#ffb08a"/> | |
| 9 | <stop offset="1" stop-color="#ffb492"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#ff8c5a"/> | |
| 13 | <stop offset="1" stop-color="#f7844f"/> | |
| 14 | </linearGradient> | |
| 15 | </defs> | |
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | |
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | |
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | |
| 19 | </svg> |
crates/canvas/assets/tags/paper.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.4" x2="0" y2="14.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf4"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M2.6 0.8H10.4L13.6 4V15.2H2.6Z" fill="#7a8494"/> | |
| 6 | <path d="M3.2 1.4H10.1L13 4.3V14.6H3.2Z" fill="url(#g0)"/> | |
| 7 | <path d="M10.1 1.4V4.3H13Z" fill="#c4cad6"/> | |
| 8 | <path d="M4.6 5.4H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 9 | <path d="M4.6 7H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 10 | <path d="M4.6 8.6H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 11 | <path d="M4.6 10.2H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 12 | <path d="M4.6 11.8H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 13 | <path d="M4.6 13.4H11.6" fill="none" stroke="#8a9ab8" stroke-width="0.6"/> | |
| 14 | </svg> |
crates/canvas/assets/tags/paperclip.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M10.2 5.4V12.6C10.2 14.8 5.8 14.8 5.8 12.6V3.4C5.8 1 9.6 1 9.6 3.4V11.4C9.6 12.6 7.6 12.6 7.6 11.4V5.2" fill="none" stroke="#5c6470" stroke-width="1.3"/> | |
| 3 | <path d="M10.2 5.4V12.6C10.2 14.8 5.8 14.8 5.8 12.6V3.4C5.8 1 9.6 1 9.6 3.4V11.4C9.6 12.6 7.6 12.6 7.6 11.4V5.2" fill="none" stroke="#d8dce4" stroke-width="0.5"/> | |
| 4 | </svg> |
crates/canvas/assets/tags/people.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.4" x2="0" y2="6.34" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8dadf"/><stop offset="1" stop-color="#7a7f88"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="6.53" x2="0" y2="11.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f4a0a0"/><stop offset="1" stop-color="#c04848"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="4.6" x2="0" y2="9.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8dadf"/><stop offset="1" stop-color="#7a7f88"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="10" x2="0" y2="15.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9ab8f0"/><stop offset="1" stop-color="#3a64b8"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <path d="M7.93 3.87A2.47 2.47 0 1 1 12.87 3.87A2.47 2.47 0 1 1 7.93 3.87Z" fill="url(#g0)"/> | |
| 9 | <path d="M10.4 6.53C13.63 6.53 14.58 7.86 14.58 9.95V11.47H6.22V9.95C6.22 7.86 7.17 6.53 10.4 6.53Z" fill="url(#g1)"/> | |
| 10 | <path d="M3 7.2A2.6 2.6 0 1 1 8.2 7.2A2.6 2.6 0 1 1 3 7.2Z" fill="url(#g2)"/> | |
| 11 | <path d="M5.6 10C9 10 10 11.4 10 13.6V15.2H1.2V13.6C1.2 11.4 2.2 10 5.6 10Z" fill="url(#g3)"/> | |
| 12 | </svg> |
crates/canvas/assets/tags/person.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4.8" x2="0" y2="10" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8dadf"/><stop offset="1" stop-color="#8e939c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="10.2" x2="0" y2="15.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c4d4f0"/><stop offset="1" stop-color="#7c94c4"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="0.6" x2="0" y2="6.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8a7a"/><stop offset="1" stop-color="#d0301a"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M3.8 7.4A2.6 2.6 0 1 1 9 7.4A2.6 2.6 0 1 1 3.8 7.4Z" fill="url(#g0)"/> | |
| 8 | <path d="M6.4 10.2C9.8 10.2 10.8 11.6 10.8 13.8V15.4H2V13.8C2 11.6 3 10.2 6.4 10.2Z" fill="url(#g1)"/> | |
| 9 | <path d="M12.2 0.6H14.2L13.6 6.4H12.8Z" fill="url(#g2)"/> | |
| 10 | <path d="M12.3 8A0.9 0.9 0 1 1 14.1 8A0.9 0.9 0 1 1 12.3 8Z" fill="#d0301a"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/phone-clock.svg created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.6" x2="0" y2="7.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e4e8ee"/><stop offset="1" stop-color="#8c94a0"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="5.8" x2="0" y2="10" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d8dce4"/><stop offset="1" stop-color="#8c94a0"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="5.4" x2="0" y2="15.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8ccd4"/><stop offset="1" stop-color="#7a808c"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="5.4" x2="0" y2="15.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf2"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <path d="M2 7.4C1.4 5 2.6 3 5.4 2.2C8 1.4 11.4 1.6 13.4 2.8C14.8 3.6 15 5 14.4 6L12.6 6.4L11.8 4.8C10 4.2 7.6 4.4 6 5L5.6 6.8Z" fill="url(#g0)"/> | |
| 9 | <path d="M2 7.4C1.4 5 2.6 3 5.4 2.2C8 1.4 11.4 1.6 13.4 2.8C14.8 3.6 15 5 14.4 6L12.6 6.4L11.8 4.8C10 4.2 7.6 4.4 6 5L5.6 6.8Z" fill="none" stroke="#5a6270" stroke-width="0.5"/> | |
| 10 | <path d="M3.6 7.4L6 5.8H10L12.4 7.4V10H3.6Z" fill="url(#g1)"/> | |
| 11 | <path d="M5 10.6A5.2 5.2 0 1 1 15.4 10.6A5.2 5.2 0 1 1 5 10.6Z" fill="url(#g2)"/> | |
| 12 | <path d="M5.9 10.6A4.3 4.3 0 1 1 14.5 10.6A4.3 4.3 0 1 1 5.9 10.6Z" fill="url(#g3)"/> | |
| 13 | <path d="M13.3 10.6A0.5 0.5 0 1 1 14.3 10.6A0.5 0.5 0 1 1 13.3 10.6Z" fill="#4a5060"/> | |
| 14 | <path d="M12.97 12.4A0.35 0.35 0 1 1 13.67 12.4A0.35 0.35 0 1 1 12.97 12.4Z" fill="#4a5060"/> | |
| 15 | <path d="M11.65 13.72A0.35 0.35 0 1 1 12.35 13.72A0.35 0.35 0 1 1 11.65 13.72Z" fill="#4a5060"/> | |
| 16 | <path d="M9.7 14.2A0.5 0.5 0 1 1 10.7 14.2A0.5 0.5 0 1 1 9.7 14.2Z" fill="#4a5060"/> | |
| 17 | <path d="M8.05 13.72A0.35 0.35 0 1 1 8.75 13.72A0.35 0.35 0 1 1 8.05 13.72Z" fill="#4a5060"/> | |
| 18 | <path d="M6.73 12.4A0.35 0.35 0 1 1 7.43 12.4A0.35 0.35 0 1 1 6.73 12.4Z" fill="#4a5060"/> | |
| 19 | <path d="M6.1 10.6A0.5 0.5 0 1 1 7.1 10.6A0.5 0.5 0 1 1 6.1 10.6Z" fill="#4a5060"/> | |
| 20 | <path d="M6.73 8.8A0.35 0.35 0 1 1 7.43 8.8A0.35 0.35 0 1 1 6.73 8.8Z" fill="#4a5060"/> | |
| 21 | <path d="M8.05 7.48A0.35 0.35 0 1 1 8.75 7.48A0.35 0.35 0 1 1 8.05 7.48Z" fill="#4a5060"/> | |
| 22 | <path d="M9.7 7A0.5 0.5 0 1 1 10.7 7A0.5 0.5 0 1 1 9.7 7Z" fill="#4a5060"/> | |
| 23 | <path d="M11.65 7.48A0.35 0.35 0 1 1 12.35 7.48A0.35 0.35 0 1 1 11.65 7.48Z" fill="#4a5060"/> | |
| 24 | <path d="M12.97 8.8A0.35 0.35 0 1 1 13.67 8.8A0.35 0.35 0 1 1 12.97 8.8Z" fill="#4a5060"/> | |
| 25 | <path d="M10.2 7.48V10.6L12.38 9.3" fill="none" stroke="#20242c" stroke-width="0.62"/> | |
| 26 | </svg> |
crates/canvas/assets/tags/pink-square.svg created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#b13a9f"/> | |
| 5 | <stop offset="1" stop-color="#962d86"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#e690d9"/> | |
| 9 | <stop offset="1" stop-color="#e894dc"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#cb45b7"/> | |
| 13 | <stop offset="1" stop-color="#c23eae"/> | |
| 14 | </linearGradient> | |
| 15 | </defs> | |
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | |
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | |
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | |
| 19 | </svg> |
crates/canvas/assets/tags/plane.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.4" x2="0" y2="15.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b4d0ff"/><stop offset="1" stop-color="#3a6ad0"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M15.2 1C14.4 0.4 13 0.8 12 1.8L9.4 4.4L2.2 2.6L0.8 4L6.6 7.2L3.8 10L1.6 9.6L0.6 10.6L3.4 12.6L5.4 15.4L6.4 14.4L6 12.2L8.8 9.4L12 15.2L13.4 13.8L11.6 6.6L14.2 4C15.2 3 15.6 1.6 15.2 1Z" fill="url(#g0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/presentation.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2.8" x2="0" y2="10.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e4e8f0"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 1.2H15.2V2.4H0.8Z" fill="#7a808c"/> | |
| 6 | <path d="M1.6 2.4H14.4V10.6H1.6Z" fill="#9aa0ac"/> | |
| 7 | <path d="M2.2 2.8H13.8V10.2H2.2Z" fill="url(#g0)"/> | |
| 8 | <path d="M8 10.6V13.6M8 13.6L5 15.4M8 13.6L11 15.4" fill="none" stroke="#7a808c" stroke-width="0.9"/> | |
| 9 | <path d="M3.2 6.4A2.4 2.4 0 1 1 8 6.4A2.4 2.4 0 1 1 3.2 6.4Z" fill="#3a6ad8"/> | |
| 10 | <path d="M5.6 6.4V4A2.4 2.4 0 0 1 7.9 5.8Z" fill="#e04040"/> | |
| 11 | <path d="M5.6 6.4L7.9 5.8A2.4 2.4 0 0 1 7.4 8Z" fill="#f0c020"/> | |
| 12 | <path d="M9.4 4.6H12.6" fill="none" stroke="#8a92a4" stroke-width="0.6"/> | |
| 13 | <path d="M9.4 6.2H12.6" fill="none" stroke="#8a92a4" stroke-width="0.6"/> | |
| 14 | <path d="M9.4 7.8H12.6" fill="none" stroke="#8a92a4" stroke-width="0.6"/> | |
| 15 | </svg> |
crates/canvas/assets/tags/pushpin.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="0.8" x2="0" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a8c8f8"/><stop offset="1" stop-color="#2e62c4"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M6.4 9.6L1.4 14.8" fill="none" stroke="#6a6a6a" stroke-width="1"/> | |
| 6 | <path d="M4.2 7.8L8.4 12L9.8 10.6C9.6 9.6 9.8 8.6 10.6 7.8L12 6.4C12.9 6.8 14.2 6.6 14.8 5.8C15.6 4.8 15.2 3.2 14 2C12.8 0.8 11.2 0.4 10.2 1.2C9.4 1.8 9.2 3.1 9.6 4L8.2 5.4C7.4 6.2 6.4 6.4 5.4 6.2Z" fill="url(#g0)"/> | |
| 7 | <path d="M10.7 3.2A1.6 1.1 0 1 1 13.9 3.2A1.6 1.1 0 1 1 10.7 3.2Z" fill="#ffffff" fill-opacity="0.6"/> | |
| 8 | </svg> |
crates/canvas/assets/tags/question-balloon.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.2" x2="0" y2="14.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff2b0"/><stop offset="1" stop-color="#e2b43a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 1.2C12.2 1.2 15 3.6 15 6.8C15 10 12.2 12.3 8.4 12.3C7.6 12.3 6.9 12.2 6.2 12L2.4 14.8L3.4 11.2C1.8 10.1 1 8.6 1 6.8C1 3.6 3.8 1.2 8 1.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M8 1.2C12.2 1.2 15 3.6 15 6.8C15 10 12.2 12.3 8.4 12.3C7.6 12.3 6.9 12.2 6.2 12L2.4 14.8L3.4 11.2C1.8 10.1 1 8.6 1 6.8C1 3.6 3.8 1.2 8 1.2Z" fill="none" stroke="#9a7a28" stroke-width="0.6"/> | |
| 7 | <path d="M6.2 5.2C6.2 3.4 9.8 3.4 9.8 5.2C9.8 6.6 8 6.6 8 8.2" fill="none" stroke="#2a2a2a" stroke-width="1.1"/> | |
| 8 | <path d="M7.25 10.1A0.75 0.75 0 1 1 8.75 10.1A0.75 0.75 0 1 1 7.25 10.1Z" fill="#2a2a2a"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/quote.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.4" x2="0" y2="14.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9ab8f4"/><stop offset="1" stop-color="#3a64c4"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M4.2 2.2C5.6 1.4 7 1.4 7.6 2.2C8.2 3.2 8.2 5.6 8 7.4C7.6 10.4 5.4 13 2.4 14.2L2 13.4C4 11.6 5 9.8 5.2 8.4C3.2 8.2 1.6 7.4 1.4 6.4C1.4 4 2.6 2.8 4.2 2.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M11.8 2.2C13.2 1.4 14.6 1.4 15.2 2.2C15.8 3.2 15.8 5.6 15.6 7.4C15.2 10.4 13 13 10 14.2L9.6 13.4C11.6 11.6 12.6 9.8 12.8 8.4C10.8 8.2 9.2 7.4 9 6.4C9 4 10.2 2.8 11.8 2.2Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/research.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b4b4e8"/><stop offset="1" stop-color="#5a5aa8"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1.2" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8c870"/><stop offset="1" stop-color="#a88420"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="3.2" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe8a0"/><stop offset="1" stop-color="#c89c30"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="7.9" x2="0" y2="12.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e8f4ff"/><stop offset="1" stop-color="#9ac0ec"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <path d="M1 2H5.6V14.4H1Z" fill="url(#g0)"/> | |
| 9 | <path d="M5.6 1.2H10V14.4H5.6Z" fill="url(#g1)"/> | |
| 10 | <path d="M9.2 3.2H12.4V14.4H9.2Z" fill="url(#g2)"/> | |
| 11 | <path d="M12.4 12.4L15.2 15.2" fill="none" stroke="#4a4a4a" stroke-width="1.6"/> | |
| 12 | <path d="M7.2 10.4A3.2 3.2 0 1 1 13.6 10.4A3.2 3.2 0 1 1 7.2 10.4Z" fill="#5a6070"/> | |
| 13 | <path d="M7.9 10.4A2.5 2.5 0 1 1 12.9 10.4A2.5 2.5 0 1 1 7.9 10.4Z" fill="url(#g3)"/> | |
| 14 | </svg> |
crates/canvas/assets/tags/ribbon.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="8" x2="0" y2="15.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff7a5a"/><stop offset="1" stop-color="#d02810"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="1" x2="0" y2="10" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8a6a"/><stop offset="1" stop-color="#e03a1a"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="3.3" x2="0" y2="7.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff4b0"/><stop offset="1" stop-color="#e8c040"/></linearGradient> | |
| 6 | </defs> | |
| 7 | <path d="M5 8L2 14.6L4.4 13.8L5.6 15.6L8 10ZM11 8L14 14.6L11.6 13.8L10.4 15.6L8 10Z" fill="url(#g0)"/> | |
| 8 | <path d="M3.2 5.6A4.8 4.8 0 1 1 12.8 5.6A4.8 4.8 0 1 1 3.2 5.6Z" fill="#c83218"/> | |
| 9 | <path d="M3.9 5.6A4.1 4.1 0 1 1 12.1 5.6A4.1 4.1 0 1 1 3.9 5.6Z" fill="url(#g1)"/> | |
| 10 | <path d="M5.7 5.6A2.3 2.3 0 1 1 10.3 5.6A2.3 2.3 0 1 1 5.7 5.6Z" fill="url(#g2)"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/rose.svg created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="7" x2="0" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8ae07a"/><stop offset="1" stop-color="#2a8a2a"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="0.6" x2="0" y2="8.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e83030"/><stop offset="1" stop-color="#8a0808"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M2 15L10.2 5.6" fill="none" stroke="#2a8a2a" stroke-width="1"/> | |
| 7 | <path d="M4.6 11.8C2.6 11.6 2 9 3.4 7C4.6 8.4 5 10.2 4.6 11.8Z" fill="url(#g0)"/> | |
| 8 | <path d="M7.6 4.4A3.8 3.8 0 1 1 15.2 4.4A3.8 3.8 0 1 1 7.6 4.4Z" fill="url(#g1)"/> | |
| 9 | <path d="M9.6 3.4C10.6 2.4 12.6 2.6 13 4.2C13.4 5.8 11.6 6.6 10.6 5.6C9.8 4.8 10.8 3.8 11.6 4.4" fill="none" stroke="#6a0404" stroke-width="0.6"/> | |
| 10 | </svg> |
crates/canvas/assets/tags/schedule.svg created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e4eaf4"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="4" x2="0" y2="6.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8fb1ec"/><stop offset="1" stop-color="#4d78c8"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="0.9" x2="0" y2="8.3" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c8ccd4"/><stop offset="1" stop-color="#7a808c"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="0.9" x2="0" y2="8.3" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#e8ecf2"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <path d="M4 4H15.4V15H4Z" fill="#7a8aac"/> | |
| 9 | <path d="M4.5 4.5H14.9V14.5H4.5Z" fill="url(#g0)"/> | |
| 10 | <path d="M4.5 4.5H14.9V6.2H4.5Z" fill="url(#g1)"/> | |
| 11 | <path d="M6.2 8H8M9.4 8H13.4" fill="none" stroke="#8a9ab8" stroke-width="0.8"/> | |
| 12 | <path d="M6.2 10.4H8M9.4 10.4H13.4" fill="none" stroke="#8a9ab8" stroke-width="0.8"/> | |
| 13 | <path d="M6.2 12.8H8M9.4 12.8H13.4" fill="none" stroke="#8a9ab8" stroke-width="0.8"/> | |
| 14 | <path d="M0.4 4.6A4.2 4.2 0 1 1 8.8 4.6A4.2 4.2 0 1 1 0.4 4.6Z" fill="#d03030"/> | |
| 15 | <path d="M0.9 4.6A3.7 3.7 0 1 1 8.3 4.6A3.7 3.7 0 1 1 0.9 4.6Z" fill="url(#g2)"/> | |
| 16 | <path d="M1.8 4.6A2.8 2.8 0 1 1 7.4 4.6A2.8 2.8 0 1 1 1.8 4.6Z" fill="url(#g3)"/> | |
| 17 | <path d="M6.2 4.6A0.5 0.5 0 1 1 7.2 4.6A0.5 0.5 0 1 1 6.2 4.6Z" fill="#4a5060"/> | |
| 18 | <path d="M6.07 5.65A0.35 0.35 0 1 1 6.77 5.65A0.35 0.35 0 1 1 6.07 5.65Z" fill="#4a5060"/> | |
| 19 | <path d="M5.3 6.42A0.35 0.35 0 1 1 6 6.42A0.35 0.35 0 1 1 5.3 6.42Z" fill="#4a5060"/> | |
| 20 | <path d="M4.1 6.7A0.5 0.5 0 1 1 5.1 6.7A0.5 0.5 0 1 1 4.1 6.7Z" fill="#4a5060"/> | |
| 21 | <path d="M3.2 6.42A0.35 0.35 0 1 1 3.9 6.42A0.35 0.35 0 1 1 3.2 6.42Z" fill="#4a5060"/> | |
| 22 | <path d="M2.43 5.65A0.35 0.35 0 1 1 3.13 5.65A0.35 0.35 0 1 1 2.43 5.65Z" fill="#4a5060"/> | |
| 23 | <path d="M2 4.6A0.5 0.5 0 1 1 3 4.6A0.5 0.5 0 1 1 2 4.6Z" fill="#4a5060"/> | |
| 24 | <path d="M2.43 3.55A0.35 0.35 0 1 1 3.13 3.55A0.35 0.35 0 1 1 2.43 3.55Z" fill="#4a5060"/> | |
| 25 | <path d="M3.2 2.78A0.35 0.35 0 1 1 3.9 2.78A0.35 0.35 0 1 1 3.2 2.78Z" fill="#4a5060"/> | |
| 26 | <path d="M4.1 2.5A0.5 0.5 0 1 1 5.1 2.5A0.5 0.5 0 1 1 4.1 2.5Z" fill="#4a5060"/> | |
| 27 | <path d="M5.3 2.78A0.35 0.35 0 1 1 6 2.78A0.35 0.35 0 1 1 5.3 2.78Z" fill="#4a5060"/> | |
| 28 | <path d="M6.07 3.55A0.35 0.35 0 1 1 6.77 3.55A0.35 0.35 0 1 1 6.07 3.55Z" fill="#4a5060"/> | |
| 29 | <path d="M4.6 2.38V4.6L6.15 3.67" fill="none" stroke="#20242c" stroke-width="0.6"/> | |
| 30 | </svg> |
crates/canvas/assets/tags/smiley.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1" x2="0" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff27a"/><stop offset="1" stop-color="#e8b400"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.6 8A7.4 7.4 0 1 1 15.4 8A7.4 7.4 0 1 1 0.6 8Z" fill="#b88a00"/> | |
| 6 | <path d="M1.2 8A6.8 6.8 0 1 1 14.8 8A6.8 6.8 0 1 1 1.2 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M3.8 4.6A4.2 2.6 0 1 1 12.2 4.6A4.2 2.6 0 1 1 3.8 4.6Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 8 | <path d="M5 6.8A0.8 1.1 0 1 1 6.6 6.8A0.8 1.1 0 1 1 5 6.8Z" fill="#2a2008"/> | |
| 9 | <path d="M9.4 6.8A0.8 1.1 0 1 1 11 6.8A0.8 1.1 0 1 1 9.4 6.8Z" fill="#2a2008"/> | |
| 10 | <path d="M4.4 9.6C5.8 12.6 10.2 12.6 11.6 9.6" fill="none" stroke="#2a2008" stroke-width="0.9"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/solid-target-blue.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="4.8" x2="0" y2="11.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#3d62ad"/> | |
| 7 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 8 | <path d="M3.6 8A4.4 4.4 0 1 1 12.4 8A4.4 4.4 0 1 1 3.6 8Z" fill="#ffffff"/> | |
| 9 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g1)"/> | |
| 10 | <path d="M4.4 4.1A3.6 1.8 0 1 1 11.6 4.1A3.6 1.8 0 1 1 4.4 4.1Z" fill="#ffffff" fill-opacity="0.35"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/solid-target-green.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="4.8" x2="0" y2="11.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#3f8a2c"/> | |
| 7 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 8 | <path d="M3.6 8A4.4 4.4 0 1 1 12.4 8A4.4 4.4 0 1 1 3.6 8Z" fill="#ffffff"/> | |
| 9 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g1)"/> | |
| 10 | <path d="M4.4 4.1A3.6 1.8 0 1 1 11.6 4.1A3.6 1.8 0 1 1 4.4 4.1Z" fill="#ffffff" fill-opacity="0.35"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/solid-target-yellow.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="4.8" x2="0" y2="11.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcf0c0"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#b0801a"/> | |
| 7 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 8 | <path d="M3.6 8A4.4 4.4 0 1 1 12.4 8A4.4 4.4 0 1 1 3.6 8Z" fill="#ffffff"/> | |
| 9 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g1)"/> | |
| 10 | <path d="M4.4 4.1A3.6 1.8 0 1 1 11.6 4.1A3.6 1.8 0 1 1 4.4 4.1Z" fill="#ffffff" fill-opacity="0.35"/> | |
| 11 | </svg> |
crates/canvas/assets/tags/star-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2" x2="0" y2="14" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 0.8L10.32 5.5L15.51 6.26L11.76 9.92L12.64 15.09L8 12.65L3.36 15.09L4.24 9.92L0.49 6.26L5.68 5.5Z" fill="#3d62ad"/> | |
| 6 | <path d="M8 2.2L9.94 6.13L14.28 6.76L11.14 9.82L11.88 14.14L8 12.1L4.12 14.14L4.86 9.82L1.72 6.76L6.06 6.13Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/star-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="2" x2="0" y2="14" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 0.8L10.32 5.5L15.51 6.26L11.76 9.92L12.64 15.09L8 12.65L3.36 15.09L4.24 9.92L0.49 6.26L5.68 5.5Z" fill="#3f8a2c"/> | |
| 6 | <path d="M8 2.2L9.94 6.13L14.28 6.76L11.14 9.82L11.88 14.14L8 12.1L4.12 14.14L4.86 9.82L1.72 6.76L6.06 6.13Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/star8-blue.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M8 1.2V14.8M1.2 8H14.8M3.2 3.2L12.8 12.8M12.8 3.2L3.2 12.8" fill="none" stroke="#4b78c9" stroke-width="1"/> | |
| 3 | <path d="M6.6 8A1.4 1.4 0 1 1 9.4 8A1.4 1.4 0 1 1 6.6 8Z" fill="#3d62ad"/> | |
| 4 | </svg> |
crates/canvas/assets/tags/star8-yellow.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M8 1.2V14.8M1.2 8H14.8M3.2 3.2L12.8 12.8M12.8 3.2L3.2 12.8" fill="none" stroke="#e1a81f" stroke-width="1"/> | |
| 3 | <path d="M6.6 8A1.4 1.4 0 1 1 9.4 8A1.4 1.4 0 1 1 6.6 8Z" fill="#b0801a"/> | |
| 4 | </svg> |
crates/canvas/assets/tags/sun-blue.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4.1" x2="0" y2="11.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0c8f2"/><stop offset="1" stop-color="#5c86d1"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M12.4 8L15.4 8" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 6 | <path d="M11.11 11.11L13.23 13.23" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 7 | <path d="M8 12.4L8 15.4" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 8 | <path d="M4.89 11.11L2.77 13.23" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 9 | <path d="M3.6 8L0.6 8" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 10 | <path d="M4.89 4.89L2.77 2.77" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 11 | <path d="M8 3.6L8 0.6" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 12 | <path d="M11.11 4.89L13.23 2.77" fill="none" stroke="#4b78c9" stroke-width="0.9"/> | |
| 13 | <path d="M4.1 8A3.9 3.9 0 1 1 11.9 8A3.9 3.9 0 1 1 4.1 8Z" fill="#3d62ad"/> | |
| 14 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g0)"/> | |
| 15 | <path d="M5.58 6.52A2.42 1.56 0 1 1 10.42 6.52A2.42 1.56 0 1 1 5.58 6.52Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 16 | </svg> |
crates/canvas/assets/tags/sun-green.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4.1" x2="0" y2="11.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#c0e6ae"/><stop offset="1" stop-color="#6cb450"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M12.4 8L15.4 8" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 6 | <path d="M11.11 11.11L13.23 13.23" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 7 | <path d="M8 12.4L8 15.4" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 8 | <path d="M4.89 11.11L2.77 13.23" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 9 | <path d="M3.6 8L0.6 8" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 10 | <path d="M4.89 4.89L2.77 2.77" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 11 | <path d="M8 3.6L8 0.6" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 12 | <path d="M11.11 4.89L13.23 2.77" fill="none" stroke="#5aa83c" stroke-width="0.9"/> | |
| 13 | <path d="M4.1 8A3.9 3.9 0 1 1 11.9 8A3.9 3.9 0 1 1 4.1 8Z" fill="#3f8a2c"/> | |
| 14 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g0)"/> | |
| 15 | <path d="M5.58 6.52A2.42 1.56 0 1 1 10.42 6.52A2.42 1.56 0 1 1 5.58 6.52Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 16 | </svg> |
crates/canvas/assets/tags/sun-yellow.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="4.1" x2="0" y2="11.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fae397"/><stop offset="1" stop-color="#e6b433"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M12.4 8L15.4 8" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 6 | <path d="M11.11 11.11L13.23 13.23" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 7 | <path d="M8 12.4L8 15.4" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 8 | <path d="M4.89 11.11L2.77 13.23" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 9 | <path d="M3.6 8L0.6 8" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 10 | <path d="M4.89 4.89L2.77 2.77" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 11 | <path d="M8 3.6L8 0.6" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 12 | <path d="M11.11 4.89L13.23 2.77" fill="none" stroke="#e1a81f" stroke-width="0.9"/> | |
| 13 | <path d="M4.1 8A3.9 3.9 0 1 1 11.9 8A3.9 3.9 0 1 1 4.1 8Z" fill="#b0801a"/> | |
| 14 | <path d="M4.8 8A3.2 3.2 0 1 1 11.2 8A3.2 3.2 0 1 1 4.8 8Z" fill="url(#g0)"/> | |
| 15 | <path d="M5.58 6.52A2.42 1.56 0 1 1 10.42 6.52A2.42 1.56 0 1 1 5.58 6.52Z" fill="#ffffff" fill-opacity="0.45"/> | |
| 16 | </svg> |
crates/canvas/assets/tags/sunflower.svg created+31| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="5.4" x2="0" y2="10.6" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 4 | <linearGradient id="g1" x1="0" y1="7.57" x2="0" y2="12.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 5 | <linearGradient id="g2" x1="0" y1="9.31" x2="0" y2="14.51" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 6 | <linearGradient id="g3" x1="0" y1="10.27" x2="0" y2="15.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 7 | <linearGradient id="g4" x1="0" y1="3.23" x2="0" y2="8.43" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 8 | <linearGradient id="g5" x1="0" y1="1.49" x2="0" y2="6.69" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 9 | <linearGradient id="g6" x1="0" y1="0.53" x2="0" y2="5.73" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 10 | <linearGradient id="g7" x1="0" y1="0.53" x2="0" y2="5.73" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 11 | <linearGradient id="g8" x1="0" y1="1.49" x2="0" y2="6.69" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 12 | <linearGradient id="g9" x1="0" y1="3.23" x2="0" y2="8.43" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff080"/><stop offset="1" stop-color="#e0a010"/></linearGradient> | |
| 13 | <linearGradient id="g10" x1="0" y1="5.2" x2="0" y2="10.8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8a5a20"/><stop offset="1" stop-color="#4a2a08"/></linearGradient> | |
| 14 | </defs> | |
| 15 | <path d="M15.6 8L13 9.3L10.4 8L13 6.7Z" fill="url(#g0)"/> | |
| 16 | <path d="M14.85 11.3L11.94 11.34L10.16 9.04L13.07 9Z" fill="url(#g1)"/> | |
| 17 | <path d="M12.74 13.94L10.1 12.72L9.5 9.88L12.13 11.1Z" fill="url(#g2)"/> | |
| 18 | <path d="M9.69 15.41L7.85 13.16L8.53 10.34L10.38 12.59Z" fill="url(#g3)"/> | |
| 19 | <path d="M6.31 15.41L5.62 12.59L7.47 10.34L8.15 13.16Z" fill="url(#g3)"/> | |
| 20 | <path d="M3.26 13.94L3.87 11.1L6.5 9.88L5.9 12.72Z" fill="url(#g2)"/> | |
| 21 | <path d="M1.15 11.3L2.93 9L5.84 9.04L4.06 11.34Z" fill="url(#g1)"/> | |
| 22 | <path d="M0.4 8L3 6.7L5.6 8L3 9.3Z" fill="url(#g0)"/> | |
| 23 | <path d="M1.15 4.7L4.06 4.66L5.84 6.96L2.93 7Z" fill="url(#g4)"/> | |
| 24 | <path d="M3.26 2.06L5.9 3.28L6.5 6.12L3.87 4.9Z" fill="url(#g5)"/> | |
| 25 | <path d="M6.31 0.59L8.15 2.84L7.47 5.66L5.62 3.41Z" fill="url(#g6)"/> | |
| 26 | <path d="M9.69 0.59L10.38 3.41L8.53 5.66L7.85 2.84Z" fill="url(#g7)"/> | |
| 27 | <path d="M12.74 2.06L12.13 4.9L9.5 6.12L10.1 3.28Z" fill="url(#g8)"/> | |
| 28 | <path d="M14.85 4.7L13.07 7L10.16 6.96L11.94 4.66Z" fill="url(#g9)"/> | |
| 29 | <path d="M4.4 8A3.6 3.6 0 1 1 11.6 8A3.6 3.6 0 1 1 4.4 8Z" fill="#3a2408"/> | |
| 30 | <path d="M5.2 8A2.8 2.8 0 1 1 10.8 8A2.8 2.8 0 1 1 5.2 8Z" fill="url(#g10)"/> | |
| 31 | </svg> |
crates/canvas/assets/tags/target-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#3d62ad"/> | |
| 6 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M4.6 8A3.4 3.4 0 1 1 11.4 8A3.4 3.4 0 1 1 4.6 8Z" fill="#3d62ad"/> | |
| 8 | <path d="M5.3 8A2.7 2.7 0 1 1 10.7 8A2.7 2.7 0 1 1 5.3 8Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/target-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#3f8a2c"/> | |
| 6 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M4.6 8A3.4 3.4 0 1 1 11.4 8A3.4 3.4 0 1 1 4.6 8Z" fill="#3f8a2c"/> | |
| 8 | <path d="M5.3 8A2.7 2.7 0 1 1 10.7 8A2.7 2.7 0 1 1 5.3 8Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/target-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.5" x2="0" y2="14.5" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcf0c0"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8A7.2 7.2 0 1 1 15.2 8A7.2 7.2 0 1 1 0.8 8Z" fill="#b0801a"/> | |
| 6 | <path d="M1.5 8A6.5 6.5 0 1 1 14.5 8A6.5 6.5 0 1 1 1.5 8Z" fill="url(#g0)"/> | |
| 7 | <path d="M4.6 8A3.4 3.4 0 1 1 11.4 8A3.4 3.4 0 1 1 4.6 8Z" fill="#b0801a"/> | |
| 8 | <path d="M5.3 8A2.7 2.7 0 1 1 10.7 8A2.7 2.7 0 1 1 5.3 8Z" fill="#ffffff"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/tick-blue.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.3" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#8eaee9"/><stop offset="1" stop-color="#3d62ad"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.6 8.6L3.7 7.2L6.4 10.6L12.8 1.3L14.6 2.5L6.6 14.4Z" fill="url(#g0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/tick-green.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.3" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a4da8c"/><stop offset="1" stop-color="#3f8a2c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.6 8.6L3.7 7.2L6.4 10.6L12.8 1.3L14.6 2.5L6.6 14.4Z" fill="url(#g0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/tick-yellow.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.3" x2="0" y2="14.4" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f7d66e"/><stop offset="1" stop-color="#b0801a"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M1.6 8.6L3.7 7.2L6.4 10.6L12.8 1.3L14.6 2.5L6.6 14.4Z" fill="url(#g0)"/> | |
| 6 | </svg> |
crates/canvas/assets/tags/triangle-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="3" x2="0" y2="14" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#8eaee9"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 1.2L15.3 14.4L0.7 14.4Z" fill="#3d62ad"/> | |
| 6 | <path d="M8 2.9L13.9 13.6L2.1 13.6Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/triangle-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="3" x2="0" y2="14" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#a4da8c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 1.2L15.3 14.4L0.7 14.4Z" fill="#3f8a2c"/> | |
| 6 | <path d="M8 2.9L13.9 13.6L2.1 13.6Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/triangle-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="3" x2="0" y2="14" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcf0c0"/><stop offset="1" stop-color="#f7d66e"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M8 1.2L15.3 14.4L0.7 14.4Z" fill="#b0801a"/> | |
| 6 | <path d="M8 2.9L13.9 13.6L2.1 13.6Z" fill="url(#g0)"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/umbrella-blue.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.6" x2="0" y2="8.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d3e1fa"/><stop offset="1" stop-color="#4b78c9"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="none" stroke="#3d62ad" stroke-width="0.6"/> | |
| 7 | <path d="M8 1.6V0.6" fill="none" stroke="#3d62ad" stroke-width="0.8"/> | |
| 8 | <path d="M8 8.1V13.3C8 15.1 5.6 15.2 5.4 13.6" fill="none" stroke="#3d62ad" stroke-width="1.1"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/umbrella-green.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.6" x2="0" y2="8.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dcf2d0"/><stop offset="1" stop-color="#5aa83c"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="none" stroke="#3f8a2c" stroke-width="0.6"/> | |
| 7 | <path d="M8 1.6V0.6" fill="none" stroke="#3f8a2c" stroke-width="0.8"/> | |
| 8 | <path d="M8 8.1V13.3C8 15.1 5.6 15.2 5.4 13.6" fill="none" stroke="#3f8a2c" stroke-width="1.1"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/umbrella-yellow.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="g0" x1="0" y1="1.6" x2="0" y2="8.2" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcf0c0"/><stop offset="1" stop-color="#e1a81f"/></linearGradient> | |
| 4 | </defs> | |
| 5 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="url(#g0)"/> | |
| 6 | <path d="M0.8 8.2C1 4.3 4.1 1.6 8 1.6C11.9 1.6 15 4.3 15.2 8.2C14 7.4 12.7 7.4 11.6 8.2C10.4 7.4 9.2 7.4 8 8.2C6.8 7.4 5.6 7.4 4.4 8.2C3.3 7.4 2 7.4 0.8 8.2Z" fill="none" stroke="#b0801a" stroke-width="0.6"/> | |
| 7 | <path d="M8 1.6V0.6" fill="none" stroke="#b0801a" stroke-width="0.8"/> | |
| 8 | <path d="M8 8.1V13.3C8 15.1 5.6 15.2 5.4 13.6" fill="none" stroke="#b0801a" stroke-width="1.1"/> | |
| 9 | </svg> |
crates/canvas/assets/tags/x-blue.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.8 2.8L13.2 13.2M13.2 2.8L2.8 13.2" fill="none" stroke="#4b78c9" stroke-width="1.7"/> | |
| 3 | </svg> |
crates/canvas/assets/tags/x-dots-blue.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3.8 3.8L12.2 12.2M12.2 3.8L3.8 12.2" fill="none" stroke="#4b78c9" stroke-width="1.7"/> | |
| 3 | <path d="M6.9 2.4A1.1 1.1 0 1 1 9.1 2.4A1.1 1.1 0 1 1 6.9 2.4Z" fill="#4b78c9"/> | |
| 4 | <path d="M6.9 13.6A1.1 1.1 0 1 1 9.1 13.6A1.1 1.1 0 1 1 6.9 13.6Z" fill="#4b78c9"/> | |
| 5 | <path d="M1.3 8A1.1 1.1 0 1 1 3.5 8A1.1 1.1 0 1 1 1.3 8Z" fill="#4b78c9"/> | |
| 6 | <path d="M12.5 8A1.1 1.1 0 1 1 14.7 8A1.1 1.1 0 1 1 12.5 8Z" fill="#4b78c9"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/x-dots-green.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3.8 3.8L12.2 12.2M12.2 3.8L3.8 12.2" fill="none" stroke="#5aa83c" stroke-width="1.7"/> | |
| 3 | <path d="M6.9 2.4A1.1 1.1 0 1 1 9.1 2.4A1.1 1.1 0 1 1 6.9 2.4Z" fill="#5aa83c"/> | |
| 4 | <path d="M6.9 13.6A1.1 1.1 0 1 1 9.1 13.6A1.1 1.1 0 1 1 6.9 13.6Z" fill="#5aa83c"/> | |
| 5 | <path d="M1.3 8A1.1 1.1 0 1 1 3.5 8A1.1 1.1 0 1 1 1.3 8Z" fill="#5aa83c"/> | |
| 6 | <path d="M12.5 8A1.1 1.1 0 1 1 14.7 8A1.1 1.1 0 1 1 12.5 8Z" fill="#5aa83c"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/x-dots-yellow.svg created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3.8 3.8L12.2 12.2M12.2 3.8L3.8 12.2" fill="none" stroke="#e1a81f" stroke-width="1.7"/> | |
| 3 | <path d="M6.9 2.4A1.1 1.1 0 1 1 9.1 2.4A1.1 1.1 0 1 1 6.9 2.4Z" fill="#e1a81f"/> | |
| 4 | <path d="M6.9 13.6A1.1 1.1 0 1 1 9.1 13.6A1.1 1.1 0 1 1 6.9 13.6Z" fill="#e1a81f"/> | |
| 5 | <path d="M1.3 8A1.1 1.1 0 1 1 3.5 8A1.1 1.1 0 1 1 1.3 8Z" fill="#e1a81f"/> | |
| 6 | <path d="M12.5 8A1.1 1.1 0 1 1 14.7 8A1.1 1.1 0 1 1 12.5 8Z" fill="#e1a81f"/> | |
| 7 | </svg> |
crates/canvas/assets/tags/x-green.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.8 2.8L13.2 13.2M13.2 2.8L2.8 13.2" fill="none" stroke="#5aa83c" stroke-width="1.7"/> | |
| 3 | </svg> |
crates/canvas/assets/tags/x-yellow.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.8 2.8L13.2 13.2M13.2 2.8L2.8 13.2" fill="none" stroke="#e1a81f" stroke-width="1.7"/> | |
| 3 | </svg> |
crates/canvas/src/bin/page_probe.rs+3| ... | ... | @@ -65,6 +65,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 65 | 65 | PageObject::Image(image) => objects.push(json!({"kind": "image", "id": image.id, |
| 66 | 66 | "layout": image.layout, "bytes": image.bytes.as_ref().map(|b| b.len()), |
| 67 | 67 | "alt": image.alt, "background": image.background})), |
| 68 | PageObject::Attachment(file) => objects.push(json!({"kind": "file", "id": file.id, | |
| 69 | "layout": file.layout, "filename": file.filename, | |
| 70 | "bytes": file.bytes.as_ref().map(|b| b.len())})), | |
| 68 | 71 | PageObject::Ink(ink) => objects.push(json!({"kind": "ink", "id": ink.id, |
| 69 | 72 | "bounds": ink.bounds(), "strokes": ink.strokes.len(), "groups": ink.groups.len()})), |
| 70 | 73 | PageObject::Unsupported(object) => objects.push(json!({"kind": "unsupported", |
crates/canvas/src/editor.rs+616-76| ... | ... | @@ -117,6 +117,9 @@ pub struct CanvasEditor { |
| 117 | 117 | /// What Select All widened to, holding until the selection is moved or set, and while the |
| 118 | 118 | /// focused outline and its selection stay as they were then. |
| 119 | 119 | whole: Option<(Whole, ExGuid, Selection)>, |
| 120 | /// The outline, caret and undo depth a run of typing left, which typing there joins as | |
| 121 | /// OneNote 2010 does until the caret moves or another kind of edit comes between. | |
| 122 | typing: Option<(ExGuid, TextPosition, usize)>, | |
| 120 | 123 | } |
| 121 | 124 | |
| 122 | 125 | /// Imported page state the editable content does not carry. |
| ... | ... | @@ -804,6 +807,12 @@ enum History { |
| 804 | 807 | index: usize, |
| 805 | 808 | image: Option<Box<onestore::page::Image>>, |
| 806 | 809 | }, |
| 810 | /// Restores the file on the page at `index` in paint order, or removes the one there | |
| 811 | /// when `None`. | |
| 812 | File { | |
| 813 | index: usize, | |
| 814 | file: Option<Box<page::Content>>, | |
| 815 | }, | |
| 807 | 816 | Remove { |
| 808 | 817 | outline: onestore::ExGuid, |
| 809 | 818 | focus: RestoreFocus, |
| ... | ... | @@ -860,6 +869,7 @@ impl CanvasEditor { |
| 860 | 869 | ops: Ok(Vec::new()), |
| 861 | 870 | stored: None, |
| 862 | 871 | whole: None, |
| 872 | typing: None, | |
| 863 | 873 | }) |
| 864 | 874 | } |
| 865 | 875 | |
| ... | ... | @@ -877,6 +887,7 @@ impl CanvasEditor { |
| 877 | 887 | .iter() |
| 878 | 888 | .filter_map(|object| match object { |
| 879 | 889 | page::Content::ReadOnly(object) => Some(object.rect()[2]), |
| 890 | page::Content::File { .. } => object.file().map(|(_, rect)| rect[2]), | |
| 880 | 891 | page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]), |
| 881 | 892 | _ => None, |
| 882 | 893 | }) |
| ... | ... | @@ -1047,7 +1058,9 @@ impl CanvasEditor { |
| 1047 | 1058 | let reaches_one = |history: &History| match history { |
| 1048 | 1059 | History::Date(_) => date_changed, |
| 1049 | 1060 | History::Paper { .. } => false, |
| 1050 | History::Image { .. } | History::Picture { .. } => objects_changed, | |
| 1061 | History::Image { .. } | History::Picture { .. } | History::File { .. } => { | |
| 1062 | objects_changed | |
| 1063 | } | |
| 1051 | 1064 | History::Draft { outlines, .. } => changed.contains(&outlines[0].id), |
| 1052 | 1065 | History::Text { outline, change } => { |
| 1053 | 1066 | changed.contains(outline) |
| ... | ... | @@ -1070,10 +1083,15 @@ impl CanvasEditor { |
| 1070 | 1083 | History::Group { entries, .. } => entries.iter().any(reaches_one), |
| 1071 | 1084 | history => reaches_one(history), |
| 1072 | 1085 | }; |
| 1086 | // A typing run goes on while the change leaves its undo step alone. | |
| 1087 | let run = self | |
| 1088 | .typing | |
| 1089 | .filter(|_| self.undo.last().is_some_and(|last| !reaches(last))); | |
| 1073 | 1090 | fresh.undo = std::mem::take(&mut self.undo) |
| 1074 | 1091 | .into_iter() |
| 1075 | 1092 | .filter(|history| !reaches(history)) |
| 1076 | 1093 | .collect(); |
| 1094 | fresh.typing = run.map(|(outline, caret, _)| (outline, caret, fresh.undo.len())); | |
| 1077 | 1095 | fresh.redo = std::mem::take(&mut self.redo) |
| 1078 | 1096 | .into_iter() |
| 1079 | 1097 | .filter(|history| !reaches(history)) |
| ... | ... | @@ -1108,6 +1126,10 @@ impl CanvasEditor { |
| 1108 | 1126 | objects.push(PageObject::Image(image.clone())); |
| 1109 | 1127 | continue; |
| 1110 | 1128 | } |
| 1129 | page::Content::File { source, .. } => { | |
| 1130 | objects.push(PageObject::Attachment(source.clone())); | |
| 1131 | continue; | |
| 1132 | } | |
| 1111 | 1133 | page::Content::Ink(ink) => { |
| 1112 | 1134 | objects.push(PageObject::Ink(ink.clone())); |
| 1113 | 1135 | continue; |
| ... | ... | @@ -1423,6 +1445,7 @@ impl CanvasEditor { |
| 1423 | 1445 | ops: Ok(Vec::new()), |
| 1424 | 1446 | stored: None, |
| 1425 | 1447 | whole: None, |
| 1448 | typing: None, | |
| 1426 | 1449 | }) |
| 1427 | 1450 | } |
| 1428 | 1451 | |
| ... | ... | @@ -1455,23 +1478,99 @@ impl CanvasEditor { |
| 1455 | 1478 | Ok(true) |
| 1456 | 1479 | } |
| 1457 | 1480 | |
| 1458 | /// Gives the page `color` (View, Page Color) and `rule_lines` (View, Rule Lines) as one | |
| 1459 | /// undo step; false when it had them. | |
| 1481 | /// Gives the page `color` (View, Page Color), `rule_lines` (View, Rule Lines) and, when | |
| 1482 | /// given, `art` in place of its background pictures, as one undo step; false when it had | |
| 1483 | /// them. | |
| 1460 | 1484 | pub fn set_paper( |
| 1461 | 1485 | &mut self, |
| 1462 | 1486 | color: Option<u32>, |
| 1463 | 1487 | rule_lines: Option<onestore::page::RuleLines>, |
| 1488 | art: Option<Vec<onestore::page::Image>>, | |
| 1464 | 1489 | ) -> bool { |
| 1465 | if (color, rule_lines) == (self.header.color, self.header.rule_lines) { | |
| 1490 | let backgrounds: Vec<usize> = (0..self.objects.len()) | |
| 1491 | .filter(|&index| { | |
| 1492 | self.objects[index] | |
| 1493 | .picture() | |
| 1494 | .is_some_and(|image| image.background) | |
| 1495 | }) | |
| 1496 | .collect(); | |
| 1497 | let art = art.filter(|art| { | |
| 1498 | art.len() != backgrounds.len() | |
| 1499 | || art.iter().zip(&backgrounds).any(|(new, &index)| { | |
| 1500 | let shown = self.objects[index].picture().unwrap(); | |
| 1501 | (&new.layout, new.size, &new.bytes) != (&shown.layout, shown.size, &shown.bytes) | |
| 1502 | }) | |
| 1503 | }); | |
| 1504 | let paper = (color, rule_lines) != (self.header.color, self.header.rule_lines); | |
| 1505 | if !paper && art.is_none() { | |
| 1466 | 1506 | return false; |
| 1467 | 1507 | } |
| 1468 | 1508 | self.finish_composition(); |
| 1469 | let before = self.paper(color, rule_lines); | |
| 1470 | self.undo.push(before); | |
| 1509 | let mut entries = Vec::new(); | |
| 1510 | if paper { | |
| 1511 | entries.push(self.paper(color, rule_lines)); | |
| 1512 | } | |
| 1513 | if let Some(art) = art { | |
| 1514 | for index in backgrounds.into_iter().rev() { | |
| 1515 | let image = self.remove_picture(index); | |
| 1516 | entries.push(History::Picture { | |
| 1517 | index, | |
| 1518 | image: Some(Box::new(image)), | |
| 1519 | }); | |
| 1520 | } | |
| 1521 | // Art lies under everything else on the page. | |
| 1522 | for image in art.into_iter().rev() { | |
| 1523 | self.add_picture(0, image); | |
| 1524 | entries.push(History::Picture { | |
| 1525 | index: 0, | |
| 1526 | image: None, | |
| 1527 | }); | |
| 1528 | } | |
| 1529 | } | |
| 1530 | self.undo.push(match entries.len() { | |
| 1531 | 1 => entries.pop().unwrap(), | |
| 1532 | _ => History::Group { | |
| 1533 | entries, | |
| 1534 | page: false, | |
| 1535 | }, | |
| 1536 | }); | |
| 1471 | 1537 | self.redo.clear(); |
| 1472 | 1538 | true |
| 1473 | 1539 | } |
| 1474 | 1540 | |
| 1541 | /// Puts page-level picture `image` at `index` in paint order, recording its op. | |
| 1542 | fn add_picture(&mut self, index: usize, image: onestore::page::Image) { | |
| 1543 | let object = PageObject::Image(image.clone()); | |
| 1544 | self.objects.insert(index, page::Content::Image(image)); | |
| 1545 | let before = self.successor(object.id()); | |
| 1546 | self.record(Ok(vec![PageOp::Add { object, before }])); | |
| 1547 | } | |
| 1548 | ||
| 1549 | /// Takes the page-level picture at `index` in paint order off the page, recording its op. | |
| 1550 | fn remove_picture(&mut self, index: usize) -> onestore::page::Image { | |
| 1551 | let image = self.objects[index].picture().unwrap().clone(); | |
| 1552 | self.objects.remove(index); | |
| 1553 | self.record(Ok(vec![PageOp::Delete { object: image.id }])); | |
| 1554 | image | |
| 1555 | } | |
| 1556 | ||
| 1557 | /// Puts a file on the page, `file` being its `Content::File`, at `index` in paint order, | |
| 1558 | /// recording its op. | |
| 1559 | fn add_file(&mut self, index: usize, file: page::Content) { | |
| 1560 | let object = PageObject::Attachment(file.file().unwrap().0.clone()); | |
| 1561 | self.objects.insert(index, file); | |
| 1562 | let before = self.successor(object.id()); | |
| 1563 | self.record(Ok(vec![PageOp::Add { object, before }])); | |
| 1564 | } | |
| 1565 | ||
| 1566 | /// Takes the file at `index` in paint order off the page, recording its op. | |
| 1567 | fn remove_file(&mut self, index: usize) -> page::Content { | |
| 1568 | let file = self.objects.remove(index); | |
| 1569 | let object = file.file().unwrap().0.id; | |
| 1570 | self.record(Ok(vec![PageOp::Delete { object }])); | |
| 1571 | file | |
| 1572 | } | |
| 1573 | ||
| 1475 | 1574 | /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry |
| 1476 | 1575 | /// restoring what the page had. |
| 1477 | 1576 | fn paper( |
| ... | ... | @@ -1754,9 +1853,12 @@ impl CanvasEditor { |
| 1754 | 1853 | Ok(()) |
| 1755 | 1854 | } |
| 1756 | 1855 | |
| 1757 | /// Origin and size of a picture the user can select; OneNote passes clicks through | |
| 1758 | /// backgrounds. | |
| 1856 | /// Origin and size of a picture or file on the page the user can select; OneNote passes | |
| 1857 | /// clicks through backgrounds. | |
| 1759 | 1858 | pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> { |
| 1859 | if let Some((_, [x0, y0, x1, y1])) = self.page_file(id) { | |
| 1860 | return Some(([x0, y0], [x1 - x0, y1 - y0])); | |
| 1861 | } | |
| 1760 | 1862 | if let Some(image) = self.image(id) { |
| 1761 | 1863 | let layout = &image.layout; |
| 1762 | 1864 | return Some(( |
| ... | ... | @@ -1776,20 +1878,50 @@ impl CanvasEditor { |
| 1776 | 1878 | self.outline_picture(id).is_some() |
| 1777 | 1879 | } |
| 1778 | 1880 | |
| 1779 | /// A picture held as a paragraph: its outline, the paragraph's container and index there, | |
| 1780 | /// and the paragraph. | |
| 1881 | /// A picture or file held as a paragraph: its outline, the paragraph's container and index | |
| 1882 | /// there, and the paragraph. | |
| 1781 | 1883 | fn outline_picture( |
| 1782 | 1884 | &self, |
| 1783 | 1885 | id: ExGuid, |
| 1784 | 1886 | ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> { |
| 1785 | 1887 | self.outlines.iter().find_map(|outline| { |
| 1786 | 1888 | descendants(outline.document.nodes(), None).find_map(|(container, index, node)| { |
| 1787 | matches!(&node.content, onestore::page::ParagraphContent::Image(image) if image.id == id) | |
| 1788 | .then_some((outline.id, container, index, node)) | |
| 1889 | holds(&node.content, id).then_some((outline.id, container, index, node)) | |
| 1789 | 1890 | }) |
| 1790 | 1891 | }) |
| 1791 | 1892 | } |
| 1792 | 1893 | |
| 1894 | /// The file a paragraph holds or the page shows, which the host opens and saves. | |
| 1895 | pub fn attachment(&self, id: ExGuid) -> Option<&onestore::page::Attachment> { | |
| 1896 | if let Some((file, _)) = self.page_file(id) { | |
| 1897 | return Some(file); | |
| 1898 | } | |
| 1899 | match &self.outline_picture(id)?.3.content { | |
| 1900 | ParagraphContent::Attachment(file) => Some(file), | |
| 1901 | _ => None, | |
| 1902 | } | |
| 1903 | } | |
| 1904 | ||
| 1905 | /// A file on the page and where its column lies, in page coordinates. | |
| 1906 | fn page_file(&self, id: ExGuid) -> Option<(&onestore::page::Attachment, [f32; 4])> { | |
| 1907 | self.objects | |
| 1908 | .iter() | |
| 1909 | .filter_map(page::Content::file) | |
| 1910 | .find(|(file, _)| file.id == id) | |
| 1911 | } | |
| 1912 | ||
| 1913 | /// Where a file draws with its name, in page coordinates. | |
| 1914 | pub fn attachment_rect(&self, id: ExGuid) -> Option<[f32; 4]> { | |
| 1915 | if let Some((_, rect)) = self.page_file(id) { | |
| 1916 | return Some(rect); | |
| 1917 | } | |
| 1918 | let (outline, ..) = self.outline_picture(id)?; | |
| 1919 | let outline = self.outlines.iter().find(|item| item.id == outline)?; | |
| 1920 | let [x0, y0, x1, y1] = outline.shaped.objects.iter().find(|o| o.id == id)?.bounds(); | |
| 1921 | let [x, y] = outline.origin(); | |
| 1922 | Some([x + x0, y + y0, x + x1, y + y1]) | |
| 1923 | } | |
| 1924 | ||
| 1793 | 1925 | /// Replaces a picture paragraph through the outline's text history. |
| 1794 | 1926 | fn edit_outline_picture( |
| 1795 | 1927 | &mut self, |
| ... | ... | @@ -1827,8 +1959,8 @@ impl CanvasEditor { |
| 1827 | 1959 | }) |
| 1828 | 1960 | } |
| 1829 | 1961 | |
| 1830 | /// Moves a picture, and resizes it when `size` differs from the stored size. A picture in | |
| 1831 | /// an outline's flow keeps its place. | |
| 1962 | /// Moves a picture or a file on the page, and resizes a picture when `size` differs from | |
| 1963 | /// the stored size. A picture in an outline's flow keeps its place. | |
| 1832 | 1964 | pub fn place_image( |
| 1833 | 1965 | &mut self, |
| 1834 | 1966 | engine: &mut TextEngine, |
| ... | ... | @@ -1844,7 +1976,7 @@ impl CanvasEditor { |
| 1844 | 1976 | if let Some((.., node)) = self.outline_picture(id) { |
| 1845 | 1977 | let mut node = node.clone(); |
| 1846 | 1978 | let onestore::page::ParagraphContent::Image(image) = &mut node.content else { |
| 1847 | unreachable!() | |
| 1979 | return Ok(()); | |
| 1848 | 1980 | }; |
| 1849 | 1981 | if crate::outline::image_size(image) == Some(size) { |
| 1850 | 1982 | return Ok(()); |
| ... | ... | @@ -1853,6 +1985,25 @@ impl CanvasEditor { |
| 1853 | 1985 | image.layout.width_set_by_user = Some(true); |
| 1854 | 1986 | return self.edit_outline_picture(engine, id, Some(node)); |
| 1855 | 1987 | } |
| 1988 | if let Some((file, _)) = self.page_file(id) { | |
| 1989 | let previous = [file.layout.x, file.layout.y]; | |
| 1990 | if previous == origin.map(Some) { | |
| 1991 | return Ok(()); | |
| 1992 | } | |
| 1993 | self.finish_composition(); | |
| 1994 | let layout = self.object_layout_mut(id).unwrap(); | |
| 1995 | [layout.x, layout.y] = origin.map(Some); | |
| 1996 | self.undo.push(History::Position { | |
| 1997 | object: id, | |
| 1998 | position: previous, | |
| 1999 | }); | |
| 2000 | self.redo.clear(); | |
| 2001 | self.record(self.placement_ops(&Placement { | |
| 2002 | id, | |
| 2003 | position: previous, | |
| 2004 | })); | |
| 2005 | return Ok(()); | |
| 2006 | } | |
| 1856 | 2007 | let image = self.image(id).ok_or(EditError::InvalidRange)?; |
| 1857 | 2008 | let mut layout = image.layout.clone(); |
| 1858 | 2009 | [layout.x, layout.y] = origin.map(Some); |
| ... | ... | @@ -1897,9 +2048,7 @@ impl CanvasEditor { |
| 1897 | 2048 | let mut passed = false; |
| 1898 | 2049 | for (_, _, node) in descendants(source.document.nodes(), None) { |
| 1899 | 2050 | match &node.content { |
| 1900 | onestore::page::ParagraphContent::Image(image) if image.id == id => { | |
| 1901 | passed = true | |
| 1902 | } | |
| 2051 | content if holds(content, id) => passed = true, | |
| 1903 | 2052 | onestore::page::ParagraphContent::Text(text) => { |
| 1904 | 2053 | let end = text.text.utf16_offset(text.text.text().len())?; |
| 1905 | 2054 | if passed { |
| ... | ... | @@ -1925,7 +2074,10 @@ impl CanvasEditor { |
| 1925 | 2074 | let index = self |
| 1926 | 2075 | .objects |
| 1927 | 2076 | .iter() |
| 1928 | .position(|object| matches!(object, page::Content::Image(image) if image.id == id)) | |
| 2077 | .position(|object| { | |
| 2078 | matches!(object, page::Content::Image(image) if image.id == id) | |
| 2079 | || object.file().is_some_and(|(file, _)| file.id == id) | |
| 2080 | }) | |
| 1929 | 2081 | .ok_or(EditError::InvalidRange)?; |
| 1930 | 2082 | // An emptied outline keeps its slot for undo after leaving `outlines`. |
| 1931 | 2083 | let editable = |object: &page::Content| match object { |
| ... | ... | @@ -1989,10 +2141,95 @@ impl CanvasEditor { |
| 1989 | 2141 | ) |
| 1990 | 2142 | } |
| 1991 | 2143 | |
| 2144 | /// Attaches `file` at the caret as OneNote 2010 does (`corpus/attachment-insert`): the | |
| 2145 | /// caret's paragraph splits there, the file takes the place of an empty first half, and the | |
| 2146 | /// caret starts the second. A caret on blank page puts the file on the page there | |
| 2147 | /// (`corpus/attachment-floating`). | |
| 2148 | pub fn insert_attachment( | |
| 2149 | &mut self, | |
| 2150 | engine: &mut TextEngine, | |
| 2151 | mut file: onestore::page::Attachment, | |
| 2152 | ) -> Result<(), EditorError> { | |
| 2153 | if let Focus::Caret { outline, .. } = &self.active { | |
| 2154 | [file.layout.x, file.layout.y] = outline.origin().map(Some); | |
| 2155 | let layout = Box::new(crate::outline::page_file(engine, &file)?); | |
| 2156 | let [x0, y0, x1, y1] = layout.bounds(); | |
| 2157 | [file.layout.max_width, file.layout.max_height] = [Some(x1 - x0), Some(y1 - y0)]; | |
| 2158 | file.layout.width_set_by_user = Some(false); | |
| 2159 | self.finish_composition(); | |
| 2160 | let index = self.objects.len(); | |
| 2161 | self.add_file( | |
| 2162 | index, | |
| 2163 | page::Content::File { | |
| 2164 | source: file, | |
| 2165 | layout, | |
| 2166 | }, | |
| 2167 | ); | |
| 2168 | self.undo.push(History::File { index, file: None }); | |
| 2169 | self.redo.clear(); | |
| 2170 | return Ok(()); | |
| 2171 | } | |
| 2172 | let [anchor, focus] = self.selection().positions; | |
| 2173 | let (start, end) = (anchor.min(focus), anchor.max(focus)); | |
| 2174 | let mut format = self.typing_format(start)?; | |
| 2175 | link::unlinked(&mut format); | |
| 2176 | let mut edit = self | |
| 2177 | .active_outline() | |
| 2178 | .document | |
| 2179 | .replace(start..end, vec![Paragraph::new(String::new(), format); 2])?; | |
| 2180 | let [head, tail, ..] = &mut edit.replacement[..] else { | |
| 2181 | unreachable!("a split holds both halves") | |
| 2182 | }; | |
| 2183 | let node = PageParagraph { | |
| 2184 | id: onestore::page::text::new_id()?, | |
| 2185 | content: ParagraphContent::Attachment(file), | |
| 2186 | style: None, | |
| 2187 | lists: Vec::new(), | |
| 2188 | tags: Vec::new(), | |
| 2189 | collapsed: false, | |
| 2190 | ..head.clone() | |
| 2191 | }; | |
| 2192 | let empty = head.text().unwrap().text.text().is_empty(); | |
| 2193 | if empty { | |
| 2194 | tail.tags = std::mem::take(&mut head.tags); | |
| 2195 | tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags); | |
| 2196 | } | |
| 2197 | edit.replacement.insert(1, node); | |
| 2198 | if empty { | |
| 2199 | edit.replacement.remove(0); | |
| 2200 | } | |
| 2201 | let caret = TextPosition { | |
| 2202 | paragraph: start.paragraph + usize::from(!empty), | |
| 2203 | offset: 0, | |
| 2204 | }; | |
| 2205 | self.commit( | |
| 2206 | engine, | |
| 2207 | edit, | |
| 2208 | Selection { | |
| 2209 | positions: [caret; 2], | |
| 2210 | affinities: [Affinity::Upstream; 2], | |
| 2211 | }, | |
| 2212 | ) | |
| 2213 | } | |
| 2214 | ||
| 1992 | 2215 | pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> { |
| 1993 | 2216 | if self.outline_picture(id).is_some() { |
| 1994 | 2217 | return self.edit_outline_picture(engine, id, None); |
| 1995 | 2218 | } |
| 2219 | if let Some(index) = self | |
| 2220 | .objects | |
| 2221 | .iter() | |
| 2222 | .position(|object| object.file().is_some_and(|(file, _)| file.id == id)) | |
| 2223 | { | |
| 2224 | self.finish_composition(); | |
| 2225 | let file = self.remove_file(index); | |
| 2226 | self.undo.push(History::File { | |
| 2227 | index, | |
| 2228 | file: Some(Box::new(file)), | |
| 2229 | }); | |
| 2230 | self.redo.clear(); | |
| 2231 | return Ok(()); | |
| 2232 | } | |
| 1996 | 2233 | let index = self |
| 1997 | 2234 | .objects |
| 1998 | 2235 | .iter() |
| ... | ... | @@ -2001,15 +2238,12 @@ impl CanvasEditor { |
| 2001 | 2238 | }) |
| 2002 | 2239 | .ok_or(EditError::InvalidRange)?; |
| 2003 | 2240 | self.finish_composition(); |
| 2004 | let page::Content::Image(image) = self.objects.remove(index) else { | |
| 2005 | unreachable!() | |
| 2006 | }; | |
| 2241 | let image = self.remove_picture(index); | |
| 2007 | 2242 | self.undo.push(History::Picture { |
| 2008 | 2243 | index, |
| 2009 | 2244 | image: Some(Box::new(image)), |
| 2010 | 2245 | }); |
| 2011 | 2246 | self.redo.clear(); |
| 2012 | self.record(Ok(vec![PageOp::Delete { object: id }])); | |
| 2013 | 2247 | Ok(()) |
| 2014 | 2248 | } |
| 2015 | 2249 | |
| ... | ... | @@ -2227,6 +2461,55 @@ impl CanvasEditor { |
| 2227 | 2461 | result |
| 2228 | 2462 | } |
| 2229 | 2463 | |
| 2464 | /// Runs `edit`, typing or a backspace at a caret, as part of the typing run the last undo | |
| 2465 | /// step holds if the caret is where that run left it and nothing came between; OneNote 2010 | |
| 2466 | /// undoes a run whole however long it pauses. | |
| 2467 | fn typed<T>( | |
| 2468 | &mut self, | |
| 2469 | edit: impl FnOnce(&mut Self) -> Result<T, EditorError>, | |
| 2470 | ) -> Result<T, EditorError> { | |
| 2471 | let [anchor, focus] = self.selection().positions; | |
| 2472 | let caret = match self.marked_range() { | |
| 2473 | Some(marked) => Some(marked.start), | |
| 2474 | None => (anchor == focus).then_some(focus), | |
| 2475 | }; | |
| 2476 | let joins = self.typing.take().is_some_and(|(outline, at, depth)| { | |
| 2477 | outline == self.active_outline().id | |
| 2478 | && caret == Some(at) | |
| 2479 | && depth == self.undo.len() | |
| 2480 | && self.pending.is_none() | |
| 2481 | }); | |
| 2482 | let depth = self.undo.len() - usize::from(joins); | |
| 2483 | let result = edit(self); | |
| 2484 | let (mut run, mut page) = (Vec::new(), false); | |
| 2485 | for entry in self.undo.split_off(depth) { | |
| 2486 | match entry { | |
| 2487 | History::Group { | |
| 2488 | entries, | |
| 2489 | page: selected, | |
| 2490 | } => { | |
| 2491 | page |= selected; | |
| 2492 | if run.is_empty() { | |
| 2493 | run = entries; | |
| 2494 | } else { | |
| 2495 | run.extend(entries); | |
| 2496 | } | |
| 2497 | } | |
| 2498 | entry => run.push(entry), | |
| 2499 | } | |
| 2500 | } | |
| 2501 | match run.len() { | |
| 2502 | 0 => return result, | |
| 2503 | 1 if !page => self.undo.extend(run), | |
| 2504 | _ => self.undo.push(History::Group { entries: run, page }), | |
| 2505 | } | |
| 2506 | if result.is_ok() { | |
| 2507 | let caret = self.selection().positions[1]; | |
| 2508 | self.typing = Some((self.active_outline().id, caret, self.undo.len())); | |
| 2509 | } | |
| 2510 | result | |
| 2511 | } | |
| 2512 | ||
| 2230 | 2513 | /// Removes the page selection's outlines, last first, but its last when `keep`, whose |
| 2231 | 2514 | /// text stays selected to be replaced. Deleting the page selection leaves a caret where |
| 2232 | 2515 | /// its first outline began, as OneNote 2010's does. |
| ... | ... | @@ -2375,6 +2658,7 @@ impl CanvasEditor { |
| 2375 | 2658 | ) -> Result<(), EditorError> { |
| 2376 | 2659 | self.finish_composition(); |
| 2377 | 2660 | self.whole = None; |
| 2661 | self.typing = None; | |
| 2378 | 2662 | if !extend && movement == Movement::Right && self.leave_equation()? { |
| 2379 | 2663 | return Ok(()); |
| 2380 | 2664 | } |
| ... | ... | @@ -2842,12 +3126,21 @@ impl CanvasEditor { |
| 2842 | 3126 | ) |
| 2843 | 3127 | } |
| 2844 | 3128 | |
| 2845 | /// Types `text` at the selection; a space ends a typed URL, which becomes a link. | |
| 3129 | /// Types `text` at the selection; a space ends a typed URL, which becomes a link. A line | |
| 3130 | /// of text joins the typing run before it. | |
| 2846 | 3131 | pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> { |
| 3132 | if text.is_empty() || text.contains('\n') { | |
| 3133 | self.insert_text(engine, text) | |
| 3134 | } else { | |
| 3135 | self.typed(|editor| editor.insert_text(engine, text)) | |
| 3136 | } | |
| 3137 | } | |
| 3138 | ||
| 3139 | fn insert_text(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> { | |
| 2847 | 3140 | if self.page_selected() { |
| 2848 | 3141 | return self.grouped(|editor| { |
| 2849 | 3142 | editor.remove_page(engine, true)?; |
| 2850 | editor.insert(engine, text) | |
| 3143 | editor.insert_text(engine, text) | |
| 2851 | 3144 | }); |
| 2852 | 3145 | } |
| 2853 | 3146 | self.leave_link_code()?; |
| ... | ... | @@ -3137,7 +3430,8 @@ impl CanvasEditor { |
| 3137 | 3430 | let selection = self.active_outline().selection; |
| 3138 | 3431 | let [anchor, focus] = selection.positions; |
| 3139 | 3432 | let mut range = anchor.min(focus)..anchor.max(focus); |
| 3140 | if range.is_empty() { | |
| 3433 | let at_caret = range.is_empty(); | |
| 3434 | if at_caret { | |
| 3141 | 3435 | let outline = self.active_outline(); |
| 3142 | 3436 | let paragraph = outline.paragraph_layout(focus.paragraph)?; |
| 3143 | 3437 | let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?; |
| ... | ... | @@ -3213,7 +3507,11 @@ impl CanvasEditor { |
| 3213 | 3507 | onestore::page::text::Affinity::Upstream, |
| 3214 | 3508 | )?; |
| 3215 | 3509 | } |
| 3216 | self.delete_range(engine, range) | |
| 3510 | if backward && at_caret { | |
| 3511 | self.typed(|editor| editor.delete_range(engine, range)) | |
| 3512 | } else { | |
| 3513 | self.delete_range(engine, range) | |
| 3514 | } | |
| 3217 | 3515 | } |
| 3218 | 3516 | |
| 3219 | 3517 | pub fn delete_to( |
| ... | ... | @@ -3226,19 +3524,26 @@ impl CanvasEditor { |
| 3226 | 3524 | } |
| 3227 | 3525 | self.finish_composition(); |
| 3228 | 3526 | let original = self.selection(); |
| 3229 | let preferred_x = self.preferred_x; | |
| 3230 | if original.positions[0] == original.positions[1] { | |
| 3527 | let (preferred_x, typing) = (self.preferred_x, self.typing); | |
| 3528 | let [caret, focus] = original.positions; | |
| 3529 | let collapsed = caret == focus; | |
| 3530 | if collapsed { | |
| 3231 | 3531 | let result = self.move_selection(engine, movement, true); |
| 3232 | 3532 | if let Err(error) = result { |
| 3233 | 3533 | self.active_outline_mut().selection = original; |
| 3234 | self.preferred_x = preferred_x; | |
| 3534 | (self.preferred_x, self.typing) = (preferred_x, typing); | |
| 3235 | 3535 | return Err(error); |
| 3236 | 3536 | } |
| 3237 | 3537 | } |
| 3238 | 3538 | let [anchor, focus] = self.selection().positions; |
| 3239 | 3539 | self.active_outline_mut().selection = original; |
| 3240 | self.preferred_x = preferred_x; | |
| 3241 | self.delete_range(engine, anchor.min(focus)..anchor.max(focus)) | |
| 3540 | (self.preferred_x, self.typing) = (preferred_x, typing); | |
| 3541 | let range = anchor.min(focus)..anchor.max(focus); | |
| 3542 | if collapsed && range.end == caret && range.start.paragraph == caret.paragraph { | |
| 3543 | self.typed(|editor| editor.delete_range(engine, range)) | |
| 3544 | } else { | |
| 3545 | self.delete_range(engine, range) | |
| 3546 | } | |
| 3242 | 3547 | } |
| 3243 | 3548 | |
| 3244 | 3549 | fn delete_range( |
| ... | ... | @@ -3326,6 +3631,7 @@ impl CanvasEditor { |
| 3326 | 3631 | history: History, |
| 3327 | 3632 | ) -> Result<History, (History, EditorError)> { |
| 3328 | 3633 | self.pending = None; |
| 3634 | self.typing = None; | |
| 3329 | 3635 | let valid = match &history { |
| 3330 | 3636 | History::Date(date) => self |
| 3331 | 3637 | .date |
| ... | ... | @@ -3363,9 +3669,20 @@ impl CanvasEditor { |
| 3363 | 3669 | index, |
| 3364 | 3670 | image: Some(_), |
| 3365 | 3671 | } => *index <= self.objects.len(), |
| 3366 | History::Picture { index, image: None } => { | |
| 3367 | matches!(self.objects.get(*index), Some(page::Content::Image(_))) | |
| 3368 | } | |
| 3672 | History::Picture { index, image: None } => self | |
| 3673 | .objects | |
| 3674 | .get(*index) | |
| 3675 | .and_then(page::Content::picture) | |
| 3676 | .is_some(), | |
| 3677 | History::File { | |
| 3678 | index, | |
| 3679 | file: Some(_), | |
| 3680 | } => *index <= self.objects.len(), | |
| 3681 | History::File { index, file: None } => self | |
| 3682 | .objects | |
| 3683 | .get(*index) | |
| 3684 | .and_then(page::Content::file) | |
| 3685 | .is_some(), | |
| 3369 | 3686 | History::Remove { outline, focus } => { |
| 3370 | 3687 | self.outlines.iter().any(|item| item.id == *outline) |
| 3371 | 3688 | && match focus { |
| ... | ... | @@ -3584,24 +3901,20 @@ impl CanvasEditor { |
| 3584 | 3901 | index, |
| 3585 | 3902 | image: match image { |
| 3586 | 3903 | Some(image) => { |
| 3587 | self.objects.insert(index, page::Content::Image(*image)); | |
| 3588 | let page::Content::Image(image) = &self.objects[index] else { | |
| 3589 | unreachable!() | |
| 3590 | }; | |
| 3591 | let ops = vec![PageOp::Add { | |
| 3592 | object: PageObject::Image(image.clone()), | |
| 3593 | before: self.successor(image.id), | |
| 3594 | }]; | |
| 3595 | self.record(Ok(ops)); | |
| 3904 | self.add_picture(index, *image); | |
| 3596 | 3905 | None |
| 3597 | 3906 | } |
| 3598 | None => match self.objects.remove(index) { | |
| 3599 | page::Content::Image(image) => { | |
| 3600 | self.record(Ok(vec![PageOp::Delete { object: image.id }])); | |
| 3601 | Some(Box::new(image)) | |
| 3602 | } | |
| 3603 | _ => unreachable!(), | |
| 3604 | }, | |
| 3907 | None => Some(Box::new(self.remove_picture(index))), | |
| 3908 | }, | |
| 3909 | }, | |
| 3910 | History::File { index, file } => History::File { | |
| 3911 | index, | |
| 3912 | file: match file { | |
| 3913 | Some(file) => { | |
| 3914 | self.add_file(index, *file); | |
| 3915 | None | |
| 3916 | } | |
| 3917 | None => Some(Box::new(self.remove_file(index))), | |
| 3605 | 3918 | }, |
| 3606 | 3919 | }, |
| 3607 | 3920 | History::Remove { outline, focus } => { |
| ... | ... | @@ -3827,9 +4140,17 @@ impl CanvasEditor { |
| 3827 | 4140 | .count() |
| 3828 | 4141 | .try_into() |
| 3829 | 4142 | .map_err(|_| EditError::TextTooLong)?; |
| 3830 | self.compose(engine, text, end..end)?; | |
| 3831 | self.finish_composition(); | |
| 3832 | Ok(()) | |
| 4143 | let lines = text.contains('\n'); | |
| 4144 | let commit = |editor: &mut Self| { | |
| 4145 | editor.compose(engine, text, end..end)?; | |
| 4146 | editor.finish_composition(); | |
| 4147 | Ok(()) | |
| 4148 | }; | |
| 4149 | if lines { | |
| 4150 | commit(self) | |
| 4151 | } else { | |
| 4152 | self.typed(commit) | |
| 4153 | } | |
| 3833 | 4154 | } else { |
| 3834 | 4155 | self.insert(engine, &text) |
| 3835 | 4156 | } |
| ... | ... | @@ -4128,6 +4449,15 @@ impl CanvasEditor { |
| 4128 | 4449 | /// Where `position` in `old` lies in `new`, the same outline changed elsewhere: in the same |
| 4129 | 4450 | /// paragraph, past what changed in its text when it lies after it; at the start of the |
| 4130 | 4451 | /// paragraph now at its place when that one is gone. |
| 4452 | /// Whether `content` is picture or file `id`. | |
| 4453 | fn holds(content: &ParagraphContent, id: ExGuid) -> bool { | |
| 4454 | match content { | |
| 4455 | ParagraphContent::Image(image) => image.id == id, | |
| 4456 | ParagraphContent::Attachment(file) => file.id == id, | |
| 4457 | _ => false, | |
| 4458 | } | |
| 4459 | } | |
| 4460 | ||
| 4131 | 4461 | fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition { |
| 4132 | 4462 | let found = old.leaf(position.paragraph).and_then(|(_, _, node)| { |
| 4133 | 4463 | let paragraph = new |
| ... | ... | @@ -5065,22 +5395,74 @@ mod tests { |
| 5065 | 5395 | editor.active_outline().shaped.paragraphs.len(), |
| 5066 | 5396 | fresh.paragraphs.len() |
| 5067 | 5397 | ); |
| 5068 | editor.undo(&mut engine).unwrap(); | |
| 5069 | assert_eq!( | |
| 5070 | editor | |
| 5071 | .active_outline() | |
| 5072 | .document | |
| 5073 | .paragraphs() | |
| 5074 | .last() | |
| 5075 | .unwrap() | |
| 5076 | .text(), | |
| 5077 | "Lasta" | |
| 5078 | ); | |
| 5398 | // The committed composition and the typing after it are one run. | |
| 5079 | 5399 | editor.undo(&mut engine).unwrap(); |
| 5080 | 5400 | assert_eq!(editor.outlines()[0].document, source); |
| 5081 | 5401 | assert_eq!(editor.active_outline().document, draft); |
| 5082 | 5402 | } |
| 5083 | 5403 | |
| 5404 | /// OneNote 2010's undo steps (Windows 7 lab, 2026-09-29): typing, backspaces and word | |
| 5405 | /// backspaces within a paragraph are one step however long the pauses; a caret move, Enter, | |
| 5406 | /// a backspace joining paragraphs, Delete, paste and a format chosen at the caret each end it. | |
| 5407 | #[test] | |
| 5408 | fn typing_runs_undo_whole_until_the_caret_moves_or_another_edit() { | |
| 5409 | let mut engine = TextEngine::default(); | |
| 5410 | let source = | |
| 5411 | TextDocument::new(vec![Paragraph::new("Base".into(), Format::default())]).unwrap(); | |
| 5412 | let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap(); | |
| 5413 | let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into(); | |
| 5414 | let text = |editor: &CanvasEditor| editor.active_outline().shown_text(); | |
| 5415 | editor.select(at(0, 4)).unwrap(); | |
| 5416 | editor.insert(&mut engine, " quick").unwrap(); | |
| 5417 | editor.insert(&mut engine, " brown").unwrap(); | |
| 5418 | editor.delete(&mut engine, true).unwrap(); | |
| 5419 | editor.delete_to(&mut engine, Movement::WordLeft).unwrap(); | |
| 5420 | editor.commit_text(&mut engine, "fox".into()).unwrap(); | |
| 5421 | editor.select(editor.selection()).unwrap(); | |
| 5422 | editor.insert(&mut engine, " jumps").unwrap(); | |
| 5423 | assert_eq!(text(&editor), "Base quick fox jumps"); | |
| 5424 | assert!(editor.undo(&mut engine).unwrap()); | |
| 5425 | assert_eq!(text(&editor), "Base"); | |
| 5426 | assert_eq!(editor.selection().positions, [at(0, 4).positions[0]; 2]); | |
| 5427 | assert!(!editor.can_undo()); | |
| 5428 | assert!(editor.redo(&mut engine).unwrap()); | |
| 5429 | assert_eq!(text(&editor), "Base quick fox jumps"); | |
| 5430 | ||
| 5431 | let mut shown = vec!["Base".to_owned(), text(&editor)]; | |
| 5432 | for movement in [Movement::Left, Movement::Right] { | |
| 5433 | editor.move_selection(&mut engine, movement, false).unwrap(); | |
| 5434 | } | |
| 5435 | editor.insert(&mut engine, "!").unwrap(); | |
| 5436 | editor.insert(&mut engine, "?").unwrap(); | |
| 5437 | shown.push(text(&editor)); | |
| 5438 | editor.enter(&mut engine, false).unwrap(); | |
| 5439 | shown.push(text(&editor)); | |
| 5440 | editor.insert(&mut engine, "ab").unwrap(); | |
| 5441 | shown.push(text(&editor)); | |
| 5442 | editor | |
| 5443 | .move_selection(&mut engine, Movement::Left, false) | |
| 5444 | .unwrap(); | |
| 5445 | editor.delete(&mut engine, false).unwrap(); | |
| 5446 | shown.push(text(&editor)); | |
| 5447 | editor.insert(&mut engine, "c").unwrap(); | |
| 5448 | shown.push(text(&editor)); | |
| 5449 | editor | |
| 5450 | .format(&mut engine, Formatting::Toggle(Toggle::Bold)) | |
| 5451 | .unwrap(); | |
| 5452 | editor.insert(&mut engine, "d").unwrap(); | |
| 5453 | editor.insert(&mut engine, "e").unwrap(); | |
| 5454 | shown.push(text(&editor)); | |
| 5455 | editor.select(at(1, 0)).unwrap(); | |
| 5456 | editor.delete(&mut engine, true).unwrap(); | |
| 5457 | shown.push(text(&editor)); | |
| 5458 | editor.insert(&mut engine, "f").unwrap(); | |
| 5459 | assert_eq!(text(&editor), "Base quick fox jumps!?facde"); | |
| 5460 | while editor.undo(&mut engine).unwrap() { | |
| 5461 | assert_eq!(text(&editor), shown.pop().unwrap()); | |
| 5462 | } | |
| 5463 | assert!(shown.is_empty()); | |
| 5464 | } | |
| 5465 | ||
| 5084 | 5466 | #[test] |
| 5085 | 5467 | fn table_horizontal_arrows_follow_reading_order() { |
| 5086 | 5468 | let mut engine = TextEngine::default(); |
| ... | ... | @@ -5659,6 +6041,80 @@ mod tests { |
| 5659 | 6041 | assert_eq!(ids(&editor), [background_id]); |
| 5660 | 6042 | } |
| 5661 | 6043 | |
| 6044 | #[test] | |
| 6045 | fn art_and_paper_change_together_as_one_step_under_the_page() { | |
| 6046 | use onestore::page::{Image, Page, PageObject}; | |
| 6047 | let mut engine = TextEngine::default(); | |
| 6048 | let image = |background, bytes: &[u8]| Image { | |
| 6049 | size: None, | |
| 6050 | id: onestore::page::text::new_id().unwrap(), | |
| 6051 | layout: onestore::document::Layout { | |
| 6052 | x: Some(-27.0), | |
| 6053 | y: Some(-3.6), | |
| 6054 | max_width: Some(174.5), | |
| 6055 | max_height: Some(640.0), | |
| 6056 | ..Default::default() | |
| 6057 | }, | |
| 6058 | bytes: Some(std::sync::Arc::from(bytes)), | |
| 6059 | alt: None, | |
| 6060 | background, | |
| 6061 | }; | |
| 6062 | let [old, picture] = [image(true, b"old art"), image(false, b"picture")]; | |
| 6063 | let mut editor = CanvasEditor::from_page( | |
| 6064 | Page { | |
| 6065 | title: String::new(), | |
| 6066 | identity: None, | |
| 6067 | created: None, | |
| 6068 | margin_origin: [36.0, 14.4], | |
| 6069 | color: None, | |
| 6070 | rule_lines: None, | |
| 6071 | definitions: BTreeMap::new(), | |
| 6072 | objects: vec![ | |
| 6073 | PageObject::Image(old.clone()), | |
| 6074 | PageObject::Image(picture.clone()), | |
| 6075 | ], | |
| 6076 | }, | |
| 6077 | &mut engine, | |
| 6078 | ) | |
| 6079 | .unwrap(); | |
| 6080 | let ids = |editor: &CanvasEditor| { | |
| 6081 | let page = editor.page().unwrap(); | |
| 6082 | let ids: Vec<_> = page.objects.iter().map(PageObject::id).collect(); | |
| 6083 | (ids, page.color) | |
| 6084 | }; | |
| 6085 | let art = || [image(true, b"art one"), image(true, b"art two")]; | |
| 6086 | let [one, two] = art(); | |
| 6087 | let blue = Some(0x00fef5ed); | |
| 6088 | assert!(editor.set_paper(blue, None, Some(vec![one.clone(), two.clone()]))); | |
| 6089 | let applied = (vec![one.id, two.id, picture.id], blue); | |
| 6090 | assert_eq!(ids(&editor), applied); | |
| 6091 | assert_eq!( | |
| 6092 | editor.take_ops().unwrap(), | |
| 6093 | [ | |
| 6094 | PageOp::Color(blue), | |
| 6095 | PageOp::Delete { object: old.id }, | |
| 6096 | PageOp::Add { | |
| 6097 | object: PageObject::Image(two.clone()), | |
| 6098 | before: Some(picture.id), | |
| 6099 | }, | |
| 6100 | PageOp::Add { | |
| 6101 | object: PageObject::Image(one.clone()), | |
| 6102 | before: Some(two.id), | |
| 6103 | }, | |
| 6104 | ] | |
| 6105 | ); | |
| 6106 | // The same art again, under fresh identities, changes nothing. | |
| 6107 | assert!(!editor.set_paper(blue, None, Some(art().into()))); | |
| 6108 | assert!(editor.undo(&mut engine).unwrap()); | |
| 6109 | assert_eq!(ids(&editor), (vec![old.id, picture.id], None)); | |
| 6110 | assert_eq!(editor.take_ops().unwrap().len(), 4); | |
| 6111 | assert!(!editor.can_undo()); | |
| 6112 | assert!(editor.redo(&mut engine).unwrap()); | |
| 6113 | assert_eq!(ids(&editor), applied); | |
| 6114 | assert!(editor.set_paper(blue, None, Some(Vec::new()))); | |
| 6115 | assert_eq!(ids(&editor), (vec![picture.id], blue)); | |
| 6116 | } | |
| 6117 | ||
| 5662 | 6118 | #[test] |
| 5663 | 6119 | fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() { |
| 5664 | 6120 | use onestore::page::{Image, Page, PageObject}; |
| ... | ... | @@ -5858,6 +6314,94 @@ mod tests { |
| 5858 | 6314 | ); |
| 5859 | 6315 | } |
| 5860 | 6316 | |
| 6317 | /// OneNote 2010 attaches at the caret (`corpus/attachment-insert`): the text after it | |
| 6318 | /// follows the file and keeps the caret, and an empty first half gives way to the file. | |
| 6319 | #[test] | |
| 6320 | fn a_file_attaches_at_the_caret_as_onenote_splits_around_it() { | |
| 6321 | use onestore::page::{Attachment, Page, PageObject, ParagraphContent}; | |
| 6322 | let mut engine = TextEngine::default(); | |
| 6323 | let source = TextOutline::new( | |
| 6324 | &mut engine, | |
| 6325 | TextDocument::new(vec![ | |
| 6326 | Paragraph::new("Before".into(), Default::default()), | |
| 6327 | Paragraph::new(String::new(), Default::default()), | |
| 6328 | ]) | |
| 6329 | .unwrap(), | |
| 6330 | 240.0, | |
| 6331 | [36.0, 36.0], | |
| 6332 | ) | |
| 6333 | .unwrap() | |
| 6334 | .snapshot(); | |
| 6335 | let mut editor = CanvasEditor::from_page( | |
| 6336 | Page { | |
| 6337 | title: String::new(), | |
| 6338 | identity: None, | |
| 6339 | created: None, | |
| 6340 | margin_origin: [36.0, 14.4], | |
| 6341 | color: None, | |
| 6342 | rule_lines: None, | |
| 6343 | definitions: BTreeMap::new(), | |
| 6344 | objects: vec![PageObject::Outline(source)], | |
| 6345 | }, | |
| 6346 | &mut engine, | |
| 6347 | ) | |
| 6348 | .unwrap(); | |
| 6349 | editor.take_ops().unwrap(); | |
| 6350 | let file = |name: &str| Attachment { | |
| 6351 | id: onestore::page::text::new_id().unwrap(), | |
| 6352 | filename: name.into(), | |
| 6353 | source_path: None, | |
| 6354 | size: Some([24.0, 24.0]), | |
| 6355 | layout: Default::default(), | |
| 6356 | bytes: Some(std::sync::Arc::from(b"payload".as_slice())), | |
| 6357 | preview: None, | |
| 6358 | recording: None, | |
| 6359 | }; | |
| 6360 | let at = |paragraph, offset| Selection { | |
| 6361 | positions: [TextPosition { paragraph, offset }; 2], | |
| 6362 | affinities: [Affinity::Downstream; 2], | |
| 6363 | }; | |
| 6364 | let shown = |editor: &CanvasEditor| { | |
| 6365 | let page = editor.page().unwrap(); | |
| 6366 | let PageObject::Outline(outline) = &page.objects[0] else { | |
| 6367 | panic!() | |
| 6368 | }; | |
| 6369 | outline | |
| 6370 | .paragraphs | |
| 6371 | .iter() | |
| 6372 | .map(|node| match &node.content { | |
| 6373 | ParagraphContent::Text(text) => text.text.text().to_owned(), | |
| 6374 | ParagraphContent::Attachment(file) => format!("[{}]", file.filename), | |
| 6375 | _ => panic!(), | |
| 6376 | }) | |
| 6377 | .collect::<Vec<_>>() | |
| 6378 | }; | |
| 6379 | ||
| 6380 | editor.select(at(0, 3)).unwrap(); | |
| 6381 | let middle = file("middle.txt"); | |
| 6382 | editor.insert_attachment(&mut engine, middle.clone()).unwrap(); | |
| 6383 | assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]); | |
| 6384 | // A file is no text position: the caret starts "ore", the second text. | |
| 6385 | assert_eq!(editor.selection().positions, at(1, 0).positions); | |
| 6386 | assert_eq!(editor.attachment(middle.id), Some(&middle)); | |
| 6387 | let ops = editor.take_ops().unwrap(); | |
| 6388 | assert!(ops.iter().any(|op| matches!(op, PageOp::Insert { paragraphs, .. } | |
| 6389 | if paragraphs.iter().any(|p| matches!(&p.content, ParagraphContent::Attachment(a) if *a == middle))))); | |
| 6390 | ||
| 6391 | editor.select(at(2, 0)).unwrap(); | |
| 6392 | editor.insert_attachment(&mut engine, file("empty.bin")).unwrap(); | |
| 6393 | assert_eq!( | |
| 6394 | shown(&editor), | |
| 6395 | ["Bef", "[middle.txt]", "ore", "[empty.bin]", ""] | |
| 6396 | ); | |
| 6397 | assert_eq!(editor.selection().positions[0].paragraph, 2); | |
| 6398 | ||
| 6399 | assert!(editor.undo(&mut engine).unwrap()); | |
| 6400 | assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]); | |
| 6401 | editor.remove_image(&mut engine, middle.id).unwrap(); | |
| 6402 | assert_eq!(shown(&editor), ["Bef", "ore", ""]); | |
| 6403 | } | |
| 6404 | ||
| 5861 | 6405 | #[test] |
| 5862 | 6406 | fn unknown_paragraphs_and_text_free_outlines_stay_on_the_page() { |
| 5863 | 6407 | use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported}; |
| ... | ... | @@ -6405,17 +6949,6 @@ mod tests { |
| 6405 | 6949 | assert_eq!(editor.active_outline().shaped.size, expected.shaped.size); |
| 6406 | 6950 | assert_eq!(editor.active_outline().document.nodes().len(), 2); |
| 6407 | 6951 | editor.undo(&mut engine).unwrap(); |
| 6408 | assert_eq!( | |
| 6409 | editor | |
| 6410 | .active_outline() | |
| 6411 | .document | |
| 6412 | .paragraphs() | |
| 6413 | .last() | |
| 6414 | .unwrap() | |
| 6415 | .text(), | |
| 6416 | "a" | |
| 6417 | ); | |
| 6418 | editor.undo(&mut engine).unwrap(); | |
| 6419 | 6952 | assert_eq!(editor.outlines[0].document, source); |
| 6420 | 6953 | assert_eq!(editor.active_outline().document.nodes().len(), 4); |
| 6421 | 6954 | } |
| ... | ... | @@ -7152,6 +7685,10 @@ mod tests { |
| 7152 | 7685 | let selection = editor.selection(); |
| 7153 | 7686 | editor.insert(&mut engine, "x").unwrap(); |
| 7154 | 7687 | assert_eq!(editor.outlines.len(), 1); |
| 7688 | // A caret move ends the typing run, so the backspace is its own step. | |
| 7689 | for movement in [Movement::Left, Movement::Right] { | |
| 7690 | editor.move_selection(&mut engine, movement, false).unwrap(); | |
| 7691 | } | |
| 7155 | 7692 | editor.delete(&mut engine, true).unwrap(); |
| 7156 | 7693 | assert!(editor.outlines.is_empty()); |
| 7157 | 7694 | assert_eq!(editor.active_outline().document, spaces); |
| ... | ... | @@ -7368,6 +7905,9 @@ mod tests { |
| 7368 | 7905 | ) |
| 7369 | 7906 | .unwrap(); |
| 7370 | 7907 | editor.insert(&mut engine, "One").unwrap(); |
| 7908 | for movement in [Movement::Left, Movement::Right] { | |
| 7909 | editor.move_selection(&mut engine, movement, false).unwrap(); | |
| 7910 | } | |
| 7371 | 7911 | let source = editor.active_outline().snapshot(); |
| 7372 | 7912 | let selection = editor.selection(); |
| 7373 | 7913 | editor.insert(&mut engine, "!").unwrap(); |
crates/canvas/src/editor/evidence.rs+9-10| ... | ... | @@ -91,13 +91,9 @@ fn open(engine: &mut TextEngine, summary: &str) -> CanvasEditor { |
| 91 | 91 | Some(label) => (label, true), |
| 92 | 92 | None => (tag, false), |
| 93 | 93 | }; |
| 94 | let tag = NoteTag::ALL | |
| 95 | .into_iter() | |
| 96 | .find(|tag| { | |
| 97 | matches!(&tag.definition().kind, Kind::TagDefinition { label: Some(name), .. } if name == label) | |
| 98 | }) | |
| 99 | .unwrap(); | |
| 100 | let definition = define(label.into(), tag.definition()); | |
| 94 | let defaults = NoteTag::defaults(); | |
| 95 | let place = defaults.iter().position(|tag| tag.label == label).unwrap(); | |
| 96 | let definition = define(label.into(), defaults[place].definition(place as u16)); | |
| 101 | 97 | node.text_mut().unwrap().tags.push(Tag { |
| 102 | 98 | definition: Some(definition), |
| 103 | 99 | action_type: None, |
| ... | ... | @@ -146,9 +142,12 @@ fn render(editor: &CanvasEditor) -> String { |
| 146 | 142 | }; |
| 147 | 143 | let checked = if tag.status & 1 == 1 { "[x]" } else { "" }; |
| 148 | 144 | ( |
| 149 | NoteTag::ALL | |
| 150 | .iter() | |
| 151 | .position(|known| known.definition().kind == *kind), | |
| 145 | NoteTag::of(kind).and_then(|(tag, action_type)| { | |
| 146 | NoteTag::defaults() | |
| 147 | .iter() | |
| 148 | .position(|known| *known == tag) | |
| 149 | .filter(|place| *place == usize::from(action_type)) | |
| 150 | }), | |
| 152 | 151 | format!("{}{checked}", label.as_deref().unwrap()), |
| 153 | 152 | ) |
| 154 | 153 | }) |
crates/canvas/src/editor/format.rs+231-233| ... | ... | @@ -5,6 +5,7 @@ use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE}; |
| 5 | 5 | use onestore::document::{Format, Kind, Tag}; |
| 6 | 6 | use onestore::page::ParagraphContent; |
| 7 | 7 | use onestore::page::text::new_id; |
| 8 | use serde::{Deserialize, Serialize}; | |
| 8 | 9 | |
| 9 | 10 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. |
| 10 | 11 | const AUTOMATIC: u32 = 0xff00_0000; |
| ... | ... | @@ -32,7 +33,9 @@ pub enum Formatting { |
| 32 | 33 | Indent, |
| 33 | 34 | Outdent, |
| 34 | 35 | Align(Alignment), |
| 35 | Tag(NoteTag), | |
| 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), | |
| 36 | 39 | /// Removes every note tag from the selected paragraphs. |
| 37 | 40 | RemoveTags, |
| 38 | 41 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. |
| ... | ... | @@ -161,40 +164,102 @@ impl ListStyle { |
| 161 | 164 | } |
| 162 | 165 | } |
| 163 | 166 | |
| 164 | /// OneNote 2010's default tags, the first nine on Ctrl+1 to Ctrl+9, as it stores their | |
| 165 | /// definitions (`corpus/structural-probe/tag-gallery.one`); declared in gallery order, their | |
| 166 | /// action types. | |
| 167 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 168 | pub enum NoteTag { | |
| 169 | ToDo, | |
| 170 | Important, | |
| 171 | Question, | |
| 172 | RememberForLater, | |
| 173 | Definition, | |
| 174 | Highlight, | |
| 175 | Contact, | |
| 176 | Address, | |
| 177 | PhoneNumber, | |
| 178 | WebSiteToVisit, | |
| 179 | Idea, | |
| 180 | Password, | |
| 181 | Critical, | |
| 182 | ProjectA, | |
| 183 | ProjectB, | |
| 184 | MovieToSee, | |
| 185 | BookToRead, | |
| 186 | MusicToListenTo, | |
| 187 | SourceForArticle, | |
| 188 | RememberForBlog, | |
| 189 | DiscussWithPersonA, | |
| 190 | DiscussWithPersonB, | |
| 191 | DiscussWithManager, | |
| 192 | SendInEmail, | |
| 193 | ScheduleMeeting, | |
| 194 | CallBack, | |
| 195 | ToDoPriority1, | |
| 196 | ToDoPriority2, | |
| 197 | ClientRequest, | |
| 167 | /// A tag of the user's list as the Customize Tags dialog edits it: what its definition | |
| 168 | /// stores besides its place in the list, which the page stores as its action type. | |
| 169 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] | |
| 170 | pub struct NoteTag { | |
| 171 | pub label: String, | |
| 172 | /// MS-ONE's NoteTagShape; 0 has no symbol. | |
| 173 | pub shape: u16, | |
| 174 | /// COLORREFs; none leaves the text's own. | |
| 175 | pub color: Option<u32>, | |
| 176 | pub highlight: Option<u32>, | |
| 177 | } | |
| 178 | ||
| 179 | /// OneNote 2010's tag list before it is customized, the first nine on Ctrl+1 to Ctrl+9 | |
| 180 | /// (`corpus/structural-probe/tag-gallery.one`): names, symbols and highlights. | |
| 181 | const DEFAULT_TAGS: [(&str, u16, Option<u32>); 29] = [ | |
| 182 | ("To Do", CHECKBOX, None), | |
| 183 | ("Important", 13, None), | |
| 184 | ("Question", 15, None), | |
| 185 | ("Remember for later", 0, Some(0x0000_ffff)), | |
| 186 | ("Definition", 0, Some(0x0000_ff00)), | |
| 187 | ("Highlight", 136, None), | |
| 188 | ("Contact", 118, None), | |
| 189 | ("Address", 23, None), | |
| 190 | ("Phone number", 18, None), | |
| 191 | ("Web site to visit", 125, None), | |
| 192 | ("Idea", 21, None), | |
| 193 | ("Password", 131, None), | |
| 194 | ("Critical", 17, None), | |
| 195 | ("Project A", 100, None), | |
| 196 | ("Project B", 101, None), | |
| 197 | ("Movie to see", 122, None), | |
| 198 | ("Book to read", 132, None), | |
| 199 | ("Music to listen to", 121, None), | |
| 200 | ("Source for article", 125, None), | |
| 201 | ("Remember for blog", 24, None), | |
| 202 | ("Discuss with <Person A>", 94, None), | |
| 203 | ("Discuss with <Person B>", 94, None), | |
| 204 | ("Discuss with manager", 95, None), | |
| 205 | ("Send in e-mail", 106, None), | |
| 206 | ("Schedule meeting", 12, None), | |
| 207 | ("Call back", 12, None), | |
| 208 | ("To Do priority 1", 28, None), | |
| 209 | ("To Do priority 2", 71, None), | |
| 210 | ("Client request", 8, None), | |
| 211 | ]; | |
| 212 | ||
| 213 | impl NoteTag { | |
| 214 | pub fn defaults() -> Vec<Self> { | |
| 215 | DEFAULT_TAGS | |
| 216 | .iter() | |
| 217 | .map(|&(label, shape, highlight)| Self { | |
| 218 | label: label.into(), | |
| 219 | shape, | |
| 220 | // The highlighting tags also set black text. | |
| 221 | color: highlight.map(|_| 0), | |
| 222 | highlight, | |
| 223 | }) | |
| 224 | .collect() | |
| 225 | } | |
| 226 | ||
| 227 | /// The definition OneNote stores for the tag at place `action_type` in the list. | |
| 228 | pub fn definition(&self, action_type: u16) -> Definition { | |
| 229 | Definition { | |
| 230 | kind: Kind::TagDefinition { | |
| 231 | label: Some(self.label.clone()), | |
| 232 | action_type: Some(action_type), | |
| 233 | shape: Some(self.shape), | |
| 234 | color: self.color, | |
| 235 | highlight: self.highlight, | |
| 236 | }, | |
| 237 | format: Format::default(), | |
| 238 | } | |
| 239 | } | |
| 240 | ||
| 241 | /// The tag a stored definition describes, and its action type. | |
| 242 | pub fn of(kind: &Kind) -> Option<(Self, u16)> { | |
| 243 | let Kind::TagDefinition { | |
| 244 | label, | |
| 245 | action_type, | |
| 246 | shape, | |
| 247 | color, | |
| 248 | highlight, | |
| 249 | } = kind | |
| 250 | else { | |
| 251 | return None; | |
| 252 | }; | |
| 253 | Some(( | |
| 254 | Self { | |
| 255 | label: label.clone().unwrap_or_default(), | |
| 256 | shape: shape.unwrap_or(0), | |
| 257 | color: *color, | |
| 258 | highlight: *highlight, | |
| 259 | }, | |
| 260 | action_type.unwrap_or(0), | |
| 261 | )) | |
| 262 | } | |
| 198 | 263 | } |
| 199 | 264 | |
| 200 | 265 | /// What the selection shows on the toolbar. |
| ... | ... | @@ -212,8 +277,8 @@ pub struct FormatState { |
| 212 | 277 | pub numbering: bool, |
| 213 | 278 | /// The library style every selected paragraph's list shares. |
| 214 | 279 | pub list: Option<ListStyle>, |
| 215 | /// Tags every selected paragraph has. | |
| 216 | pub tags: Vec<NoteTag>, | |
| 280 | /// Tags every selected paragraph has, with their action types. | |
| 281 | pub tags: Vec<(NoteTag, u16)>, | |
| 217 | 282 | } |
| 218 | 283 | |
| 219 | 284 | impl Toggle { |
| ... | ... | @@ -249,133 +314,6 @@ impl Toggle { |
| 249 | 314 | } |
| 250 | 315 | } |
| 251 | 316 | |
| 252 | impl NoteTag { | |
| 253 | pub const ALL: [Self; 29] = [ | |
| 254 | Self::ToDo, | |
| 255 | Self::Important, | |
| 256 | Self::Question, | |
| 257 | Self::RememberForLater, | |
| 258 | Self::Definition, | |
| 259 | Self::Highlight, | |
| 260 | Self::Contact, | |
| 261 | Self::Address, | |
| 262 | Self::PhoneNumber, | |
| 263 | Self::WebSiteToVisit, | |
| 264 | Self::Idea, | |
| 265 | Self::Password, | |
| 266 | Self::Critical, | |
| 267 | Self::ProjectA, | |
| 268 | Self::ProjectB, | |
| 269 | Self::MovieToSee, | |
| 270 | Self::BookToRead, | |
| 271 | Self::MusicToListenTo, | |
| 272 | Self::SourceForArticle, | |
| 273 | Self::RememberForBlog, | |
| 274 | Self::DiscussWithPersonA, | |
| 275 | Self::DiscussWithPersonB, | |
| 276 | Self::DiscussWithManager, | |
| 277 | Self::SendInEmail, | |
| 278 | Self::ScheduleMeeting, | |
| 279 | Self::CallBack, | |
| 280 | Self::ToDoPriority1, | |
| 281 | Self::ToDoPriority2, | |
| 282 | Self::ClientRequest, | |
| 283 | ]; | |
| 284 | ||
| 285 | /// The tag's name, as OneNote stores and lists it. | |
| 286 | pub const fn label(self) -> &'static str { | |
| 287 | match self { | |
| 288 | Self::ToDo => "To Do", | |
| 289 | Self::Important => "Important", | |
| 290 | Self::Question => "Question", | |
| 291 | Self::RememberForLater => "Remember for later", | |
| 292 | Self::Definition => "Definition", | |
| 293 | Self::Highlight => "Highlight", | |
| 294 | Self::Contact => "Contact", | |
| 295 | Self::Address => "Address", | |
| 296 | Self::PhoneNumber => "Phone number", | |
| 297 | Self::WebSiteToVisit => "Web site to visit", | |
| 298 | Self::Idea => "Idea", | |
| 299 | Self::Password => "Password", | |
| 300 | Self::Critical => "Critical", | |
| 301 | Self::ProjectA => "Project A", | |
| 302 | Self::ProjectB => "Project B", | |
| 303 | Self::MovieToSee => "Movie to see", | |
| 304 | Self::BookToRead => "Book to read", | |
| 305 | Self::MusicToListenTo => "Music to listen to", | |
| 306 | Self::SourceForArticle => "Source for article", | |
| 307 | Self::RememberForBlog => "Remember for blog", | |
| 308 | Self::DiscussWithPersonA => "Discuss with <Person A>", | |
| 309 | Self::DiscussWithPersonB => "Discuss with <Person B>", | |
| 310 | Self::DiscussWithManager => "Discuss with manager", | |
| 311 | Self::SendInEmail => "Send in e-mail", | |
| 312 | Self::ScheduleMeeting => "Schedule meeting", | |
| 313 | Self::CallBack => "Call back", | |
| 314 | Self::ToDoPriority1 => "To Do priority 1", | |
| 315 | Self::ToDoPriority2 => "To Do priority 2", | |
| 316 | Self::ClientRequest => "Client request", | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | /// The symbol OneNote draws for the tag; 0 draws none. | |
| 321 | pub const fn shape(self) -> u16 { | |
| 322 | match self { | |
| 323 | Self::ToDo => CHECKBOX, | |
| 324 | Self::Important => 13, | |
| 325 | Self::Question => 15, | |
| 326 | Self::RememberForLater | Self::Definition => 0, | |
| 327 | Self::Highlight => 136, | |
| 328 | Self::Contact => 118, | |
| 329 | Self::Address => 23, | |
| 330 | Self::PhoneNumber => 18, | |
| 331 | Self::WebSiteToVisit | Self::SourceForArticle => 125, | |
| 332 | Self::Idea => 21, | |
| 333 | Self::Password => 131, | |
| 334 | Self::Critical => 17, | |
| 335 | Self::ProjectA => 100, | |
| 336 | Self::ProjectB => 101, | |
| 337 | Self::MovieToSee => 122, | |
| 338 | Self::BookToRead => 132, | |
| 339 | Self::MusicToListenTo => 121, | |
| 340 | Self::RememberForBlog => 24, | |
| 341 | Self::DiscussWithPersonA | Self::DiscussWithPersonB => 94, | |
| 342 | Self::DiscussWithManager => 95, | |
| 343 | Self::SendInEmail => 106, | |
| 344 | Self::ScheduleMeeting | Self::CallBack => 12, | |
| 345 | Self::ToDoPriority1 => 28, | |
| 346 | Self::ToDoPriority2 => 71, | |
| 347 | Self::ClientRequest => 8, | |
| 348 | } | |
| 349 | } | |
| 350 | ||
| 351 | pub(super) fn definition(self) -> Definition { | |
| 352 | let highlight = match self { | |
| 353 | Self::RememberForLater => Some(0x0000_ffff), | |
| 354 | Self::Definition => Some(0x0000_ff00), | |
| 355 | _ => None, | |
| 356 | }; | |
| 357 | Definition { | |
| 358 | kind: Kind::TagDefinition { | |
| 359 | label: Some(self.label().into()), | |
| 360 | action_type: Some(self as u16), | |
| 361 | shape: Some(self.shape()), | |
| 362 | // The highlighting tags also set black text. | |
| 363 | color: highlight.map(|_| 0), | |
| 364 | highlight, | |
| 365 | }, | |
| 366 | format: Format::default(), | |
| 367 | } | |
| 368 | } | |
| 369 | } | |
| 370 | ||
| 371 | /// Whether tag symbol `shape` is a check box, which OneNote stores unchecked when applied. | |
| 372 | fn checkable(shape: Option<u16>) -> bool { | |
| 373 | matches!( | |
| 374 | shape.and_then(|shape| crate::outline::TagIcon::of(shape, false)), | |
| 375 | Some(crate::outline::TagIcon::CheckBox { .. }) | |
| 376 | ) | |
| 377 | } | |
| 378 | ||
| 379 | 317 | /// Where a selection starts and ends, as text leaf identity and byte offset. |
| 380 | 318 | type Ends = [(ExGuid, usize); 2]; |
| 381 | 319 | |
| ... | ... | @@ -664,15 +602,19 @@ impl CanvasEditor { |
| 664 | 602 | bullets: list(Formatting::Bullets), |
| 665 | 603 | numbering: list(Formatting::Numbering), |
| 666 | 604 | list: common(paragraphs.iter().map(|node| self.list_style(node))), |
| 667 | tags: NoteTag::ALL | |
| 668 | .into_iter() | |
| 669 | .filter(|tag| { | |
| 670 | let kind = tag.definition().kind; | |
| 671 | paragraphs | |
| 672 | .iter() | |
| 673 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) | |
| 674 | }) | |
| 675 | .collect(), | |
| 605 | tags: paragraphs.first().map_or_else(Vec::new, |first| { | |
| 606 | let mut shared: Vec<_> = tags(first) | |
| 607 | .filter_map(|tag| NoteTag::of(self.tag_kind(tag)?)) | |
| 608 | .filter(|(tag, action_type)| { | |
| 609 | let kind = tag.definition(*action_type).kind; | |
| 610 | paragraphs | |
| 611 | .iter() | |
| 612 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) | |
| 613 | }) | |
| 614 | .collect(); | |
| 615 | shared.sort_by_key(|(_, action_type)| *action_type); | |
| 616 | shared | |
| 617 | }), | |
| 676 | 618 | }) |
| 677 | 619 | } |
| 678 | 620 | |
| ... | ... | @@ -702,7 +644,7 @@ impl CanvasEditor { |
| 702 | 644 | | Formatting::Bullets |
| 703 | 645 | | Formatting::Numbering |
| 704 | 646 | | Formatting::List(_) |
| 705 | | Formatting::Tag(_) | |
| 647 | | Formatting::Tag(..) | |
| 706 | 648 | | Formatting::RemoveTags |
| 707 | 649 | | Formatting::Check |
| 708 | 650 | if title => |
| ... | ... | @@ -783,14 +725,8 @@ impl CanvasEditor { |
| 783 | 725 | } |
| 784 | 726 | }); |
| 785 | 727 | } |
| 786 | Formatting::Tag(tag) => { | |
| 787 | let definition = tag.definition(); | |
| 788 | let Kind::TagDefinition { | |
| 789 | action_type, shape, .. | |
| 790 | } = definition.kind | |
| 791 | else { | |
| 792 | unreachable!() | |
| 793 | }; | |
| 728 | Formatting::Tag(tag, action_type) => { | |
| 729 | let definition = tag.definition(*action_type); | |
| 794 | 730 | let existing = self |
| 795 | 731 | .definitions |
| 796 | 732 | .iter() |
| ... | ... | @@ -821,12 +757,12 @@ impl CanvasEditor { |
| 821 | 757 | Some(kind) if remove => *kind != definition.kind, |
| 822 | 758 | Some(Kind::TagDefinition { |
| 823 | 759 | action_type: other, .. |
| 824 | }) => *other != action_type, | |
| 760 | }) => *other != Some(*action_type), | |
| 825 | 761 | _ => true, |
| 826 | 762 | }); |
| 827 | 763 | } |
| 828 | 764 | if !remove { |
| 829 | let checkable = checkable(shape); | |
| 765 | let checkable = crate::outline::checkable(tag.shape); | |
| 830 | 766 | // Stored newest first. |
| 831 | 767 | text.tags.insert( |
| 832 | 768 | 0, |
| ... | ... | @@ -967,7 +903,7 @@ impl CanvasEditor { |
| 967 | 903 | Formatting::Toggle(toggle) => state.toggles.contains(toggle), |
| 968 | 904 | Formatting::Bullets => state.bullets, |
| 969 | 905 | Formatting::Numbering => state.numbering, |
| 970 | Formatting::Tag(tag) => state.tags.contains(tag), | |
| 906 | Formatting::Tag(tag, action_type) => state.tags.contains(&(tag.clone(), *action_type)), | |
| 971 | 907 | _ => false, |
| 972 | 908 | }; |
| 973 | 909 | let mut outlines = Vec::new(); |
| ... | ... | @@ -1038,7 +974,8 @@ impl CanvasEditor { |
| 1038 | 974 | let checkable = |tag: &Tag| { |
| 1039 | 975 | matches!( |
| 1040 | 976 | self.tag_kind(tag), |
| 1041 | Some(Kind::TagDefinition { shape, .. }) if checkable(*shape) | |
| 977 | Some(Kind::TagDefinition { shape: Some(shape), .. }) | |
| 978 | if crate::outline::checkable(*shape) | |
| 1042 | 979 | ) |
| 1043 | 980 | }; |
| 1044 | 981 | let checked = self::covered(replacement, ends) |
| ... | ... | @@ -1823,7 +1760,10 @@ mod tests { |
| 1823 | 1760 | let mut editor = plain(&mut engine, &["task", "other"]); |
| 1824 | 1761 | editor.select([at(0, 0), at(1, 5)].into()).unwrap(); |
| 1825 | 1762 | editor |
| 1826 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1763 | .format( | |
| 1764 | &mut engine, | |
| 1765 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1766 | ) | |
| 1827 | 1767 | .unwrap(); |
| 1828 | 1768 | editor.select([at(1, 2); 2].into()).unwrap(); |
| 1829 | 1769 | let outline = editor.active_outline().id; |
| ... | ... | @@ -1886,27 +1826,33 @@ mod tests { |
| 1886 | 1826 | let original = editor.active_outline().document.clone(); |
| 1887 | 1827 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 1888 | 1828 | editor |
| 1889 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1829 | .format( | |
| 1830 | &mut engine, | |
| 1831 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1832 | ) | |
| 1890 | 1833 | .unwrap(); |
| 1891 | 1834 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]); |
| 1892 | 1835 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]); |
| 1893 | assert_eq!(editor.format_state().unwrap().tags, [NoteTag::ToDo]); | |
| 1836 | assert_eq!( | |
| 1837 | editor.format_state().unwrap().tags, | |
| 1838 | [(NoteTag::defaults()[0].clone(), 0)] | |
| 1839 | ); | |
| 1894 | 1840 | let icon = |
| 1895 | 1841 | |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon; |
| 1896 | 1842 | assert_eq!( |
| 1897 | 1843 | icon(&editor), |
| 1898 | crate::outline::TagIcon::CheckBox { | |
| 1899 | checked: false, | |
| 1900 | mark: None | |
| 1844 | crate::outline::TagIcon::Symbol { | |
| 1845 | shape: 3, | |
| 1846 | checked: false | |
| 1901 | 1847 | } |
| 1902 | 1848 | ); |
| 1903 | 1849 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 1904 | 1850 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 1905 | 1851 | assert_eq!( |
| 1906 | 1852 | icon(&editor), |
| 1907 | crate::outline::TagIcon::CheckBox { | |
| 1908 | checked: true, | |
| 1909 | mark: None | |
| 1853 | crate::outline::TagIcon::Symbol { | |
| 1854 | shape: 3, | |
| 1855 | checked: true | |
| 1910 | 1856 | } |
| 1911 | 1857 | ); |
| 1912 | 1858 | editor.select([at(1, 0); 2].into()).unwrap(); |
| ... | ... | @@ -1918,27 +1864,39 @@ mod tests { |
| 1918 | 1864 | |
| 1919 | 1865 | editor.select([at(1, 0), at(2, 5)].into()).unwrap(); |
| 1920 | 1866 | editor |
| 1921 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) | |
| 1867 | .format( | |
| 1868 | &mut engine, | |
| 1869 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), | |
| 1870 | ) | |
| 1922 | 1871 | .unwrap(); |
| 1923 | 1872 | assert_eq!( |
| 1924 | 1873 | text_tags(&editor, 1), |
| 1925 | 1874 | [("Question".into(), 1, true), ("To Do".into(), 1, true)] |
| 1926 | 1875 | ); |
| 1927 | 1876 | assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]); |
| 1928 | assert_eq!(editor.format_state().unwrap().tags, [NoteTag::Question]); | |
| 1877 | assert_eq!( | |
| 1878 | editor.format_state().unwrap().tags, | |
| 1879 | [(NoteTag::defaults()[2].clone(), 2)] | |
| 1880 | ); | |
| 1929 | 1881 | let questions = editor |
| 1930 | 1882 | .definitions |
| 1931 | 1883 | .values() |
| 1932 | .filter(|definition| **definition == NoteTag::Question.definition()) | |
| 1884 | .filter(|definition| **definition == NoteTag::defaults()[2].definition(2)) | |
| 1933 | 1885 | .count(); |
| 1934 | 1886 | assert_eq!(questions, 1); |
| 1935 | 1887 | editor |
| 1936 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) | |
| 1888 | .format( | |
| 1889 | &mut engine, | |
| 1890 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), | |
| 1891 | ) | |
| 1937 | 1892 | .unwrap(); |
| 1938 | 1893 | assert_eq!(text_tags(&editor, 2), []); |
| 1939 | 1894 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 1940 | 1895 | editor |
| 1941 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1896 | .format( | |
| 1897 | &mut engine, | |
| 1898 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1899 | ) | |
| 1942 | 1900 | .unwrap(); |
| 1943 | 1901 | assert_eq!(text_tags(&editor, 0), []); |
| 1944 | 1902 | for _ in 0..7 { |
| ... | ... | @@ -1961,44 +1919,35 @@ mod tests { |
| 1961 | 1919 | .values() |
| 1962 | 1920 | .filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. })) |
| 1963 | 1921 | .collect::<Vec<_>>(); |
| 1964 | assert_eq!(stored.len(), NoteTag::ALL.len()); | |
| 1965 | for tag in NoteTag::ALL { | |
| 1966 | assert!(stored.contains(&&tag.definition()), "{tag:?}"); | |
| 1922 | let defaults = NoteTag::defaults(); | |
| 1923 | assert_eq!(stored.len(), defaults.len()); | |
| 1924 | for (place, tag) in defaults.iter().enumerate() { | |
| 1925 | assert!(stored.contains(&&tag.definition(place as u16)), "{tag:?}"); | |
| 1967 | 1926 | } |
| 1968 | 1927 | let mut engine = TextEngine::default(); |
| 1969 | 1928 | let mut editor = plain(&mut engine, &["tagged"]); |
| 1970 | for tag in NoteTag::ALL { | |
| 1971 | editor.format(&mut engine, Formatting::Tag(tag)).unwrap(); | |
| 1929 | for (place, tag) in defaults.iter().enumerate() { | |
| 1930 | editor | |
| 1931 | .format(&mut engine, Formatting::Tag(tag.clone(), place as u16)) | |
| 1932 | .unwrap(); | |
| 1972 | 1933 | } |
| 1973 | assert_eq!(editor.format_state().unwrap().tags, NoteTag::ALL); | |
| 1934 | assert_eq!(editor.format_state().unwrap().tags.len(), defaults.len()); | |
| 1974 | 1935 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; |
| 1975 | use crate::outline::TagIcon; | |
| 1976 | let icons = paragraph | |
| 1936 | let shapes = paragraph | |
| 1977 | 1937 | .tags |
| 1978 | 1938 | .iter() |
| 1979 | .map(|tag| tag.icon) | |
| 1939 | .map(|tag| match tag.icon { | |
| 1940 | crate::outline::TagIcon::Symbol { shape, .. } => shape, | |
| 1941 | crate::outline::TagIcon::Task => unreachable!(), | |
| 1942 | }) | |
| 1980 | 1943 | .collect::<Vec<_>>(); |
| 1981 | assert_eq!(icons.len(), NoteTag::ALL.len() - 2); | |
| 1982 | assert_eq!( | |
| 1983 | icons[..7], | |
| 1984 | [ | |
| 1985 | TagIcon::CheckBox { | |
| 1986 | checked: false, | |
| 1987 | mark: None | |
| 1988 | }, | |
| 1989 | TagIcon::Star, | |
| 1990 | TagIcon::Question, | |
| 1991 | TagIcon::Highlight, | |
| 1992 | TagIcon::Contact, | |
| 1993 | TagIcon::Address, | |
| 1994 | TagIcon::Phone, | |
| 1995 | ] | |
| 1996 | ); | |
| 1944 | assert_eq!(shapes.len(), defaults.len() - 2); | |
| 1945 | assert_eq!(shapes[..7], [3, 13, 15, 136, 118, 23, 18]); | |
| 1997 | 1946 | // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back, |
| 1998 | 1947 | // the priorities and Client request. |
| 1999 | let checkable = NoteTag::ALL | |
| 2000 | .into_iter() | |
| 2001 | .filter(|tag| checkable(Some(tag.shape()))) | |
| 1948 | let checkable = defaults | |
| 1949 | .iter() | |
| 1950 | .filter(|tag| crate::outline::checkable(tag.shape)) | |
| 2002 | 1951 | .count(); |
| 2003 | 1952 | assert_eq!(checkable, 9); |
| 2004 | 1953 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. |
| ... | ... | @@ -2011,6 +1960,46 @@ mod tests { |
| 2011 | 1960 | assert!(paragraph.text.backgrounds().next().is_some()); |
| 2012 | 1961 | } |
| 2013 | 1962 | |
| 1963 | /// A customized tag stores its name, symbol and colours with its place in the list as | |
| 1964 | /// its action type; applied from another place it takes a definition of its own, as | |
| 1965 | /// OneNote 2010's does (`corpus/custom-tags`). | |
| 1966 | #[test] | |
| 1967 | fn custom_tags_store_their_look_and_place() { | |
| 1968 | let mut engine = TextEngine::default(); | |
| 1969 | let mut editor = plain(&mut engine, &["one", "two"]); | |
| 1970 | let tag = NoteTag { | |
| 1971 | label: "Snow check".into(), | |
| 1972 | shape: 61, | |
| 1973 | color: Some(0x0000_0080), | |
| 1974 | highlight: Some(0x00ff_cc00), | |
| 1975 | }; | |
| 1976 | editor | |
| 1977 | .format(&mut engine, Formatting::Tag(tag.clone(), 0)) | |
| 1978 | .unwrap(); | |
| 1979 | assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 0)]); | |
| 1980 | assert_eq!(text_tags(&editor, 0), [("Snow check".into(), 1, true)]); | |
| 1981 | editor.select([at(1, 0); 2].into()).unwrap(); | |
| 1982 | editor | |
| 1983 | .format(&mut engine, Formatting::Tag(tag.clone(), 1)) | |
| 1984 | .unwrap(); | |
| 1985 | assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 1)]); | |
| 1986 | let mut stored: Vec<_> = editor | |
| 1987 | .definitions | |
| 1988 | .values() | |
| 1989 | .filter_map(|definition| NoteTag::of(&definition.kind)) | |
| 1990 | .collect(); | |
| 1991 | stored.sort_by_key(|(_, action_type)| *action_type); | |
| 1992 | assert_eq!(stored, [(tag.clone(), 0), (tag, 1)]); | |
| 1993 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; | |
| 1994 | assert!( | |
| 1995 | paragraph | |
| 1996 | .text | |
| 1997 | .backgrounds() | |
| 1998 | .all(|(_, color)| color == 0x00ff_cc00) | |
| 1999 | ); | |
| 2000 | assert!(paragraph.text.backgrounds().next().is_some()); | |
| 2001 | } | |
| 2002 | ||
| 2014 | 2003 | #[test] |
| 2015 | 2004 | fn commands_reach_every_cell_a_selection_crosses() { |
| 2016 | 2005 | let mut engine = TextEngine::default(); |
| ... | ... | @@ -2049,7 +2038,10 @@ mod tests { |
| 2049 | 2038 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 2050 | 2039 | assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]); |
| 2051 | 2040 | editor |
| 2052 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 2041 | .format( | |
| 2042 | &mut engine, | |
| 2043 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 2044 | ) | |
| 2053 | 2045 | .unwrap(); |
| 2054 | 2046 | assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1); |
| 2055 | 2047 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); |
| ... | ... | @@ -2110,8 +2102,8 @@ mod tests { |
| 2110 | 2102 | Formatting::Highlight(Some(0x0000_ffff)), |
| 2111 | 2103 | Formatting::Align(Alignment::Center), |
| 2112 | 2104 | Formatting::Numbering, |
| 2113 | Formatting::Tag(NoteTag::ToDo), | |
| 2114 | Formatting::Tag(NoteTag::Question), | |
| 2105 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 2106 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), | |
| 2115 | 2107 | Formatting::Check, |
| 2116 | 2108 | ] { |
| 2117 | 2109 | editor.format(&mut engine, command).unwrap(); |
| ... | ... | @@ -2136,7 +2128,10 @@ mod tests { |
| 2136 | 2128 | bullets: false, |
| 2137 | 2129 | numbering: true, |
| 2138 | 2130 | list: Some(ListStyle::NUMBER), |
| 2139 | tags: vec![NoteTag::ToDo, NoteTag::Question], | |
| 2131 | tags: vec![ | |
| 2132 | (NoteTag::defaults()[0].clone(), 0), | |
| 2133 | (NoteTag::defaults()[2].clone(), 2) | |
| 2134 | ], | |
| 2140 | 2135 | } |
| 2141 | 2136 | ); |
| 2142 | 2137 | // Format Painter, a gallery tag and an inserted table write as well. |
| ... | ... | @@ -2155,7 +2150,10 @@ mod tests { |
| 2155 | 2150 | ) |
| 2156 | 2151 | ); |
| 2157 | 2152 | editor |
| 2158 | .format(&mut engine, Formatting::Tag(NoteTag::CallBack)) | |
| 2153 | .format( | |
| 2154 | &mut engine, | |
| 2155 | Formatting::Tag(NoteTag::defaults()[25].clone(), 25), | |
| 2156 | ) | |
| 2159 | 2157 | .unwrap(); |
| 2160 | 2158 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 2161 | 2159 | editor.insert_table(&mut engine, 2, 3).unwrap(); |
crates/canvas/src/editor/ops.rs+4| ... | ... | @@ -330,6 +330,9 @@ impl CanvasEditor { |
| 330 | 330 | page::Content::Ink(ink) if ink.id == placement.id => { |
| 331 | 331 | return layout_ops(ink.id, &old, &moved(&ink.layout)); |
| 332 | 332 | } |
| 333 | page::Content::File { source, .. } if source.id == placement.id => { | |
| 334 | return layout_ops(source.id, &old, &moved(&source.layout)); | |
| 335 | } | |
| 333 | 336 | page::Content::Outline { source, .. } if source.id == placement.id => { |
| 334 | 337 | return layout_ops(source.id, &old, &moved(&source.layout)); |
| 335 | 338 | } |
| ... | ... | @@ -434,6 +437,7 @@ impl CanvasEditor { |
| 434 | 437 | page::Content::Outline { source, .. } => source.id, |
| 435 | 438 | page::Content::Date { .. } => continue, |
| 436 | 439 | page::Content::Image(image) => image.id, |
| 440 | page::Content::File { source, .. } => source.id, | |
| 437 | 441 | page::Content::Ink(ink) => ink.id, |
| 438 | 442 | page::Content::ReadOnly(object) |
| 439 | 443 | if matches!(object.source, PageObject::Title(_)) => |
crates/canvas/src/editor/page.rs+43-1| ... | ... | @@ -5,7 +5,7 @@ use crate::{ |
| 5 | 5 | outline::{Arrange, OutlineLayout}, |
| 6 | 6 | }; |
| 7 | 7 | use onestore::page::text::Paragraph; |
| 8 | use onestore::page::{Image, Ink, Outline, Page, PageObject}; | |
| 8 | use onestore::page::{Attachment, Image, Ink, Outline, Page, PageObject}; | |
| 9 | 9 | use std::collections::BTreeMap; |
| 10 | 10 | |
| 11 | 11 | /// A title object's own state, plus the child origins `build` replaces with page coordinates. |
| ... | ... | @@ -33,6 +33,11 @@ pub(crate) enum Content { |
| 33 | 33 | below_title: Option<onestore::ExGuid>, |
| 34 | 34 | }, |
| 35 | 35 | Image(Image), |
| 36 | /// A file on the page, laid out relative to its position. | |
| 37 | File { | |
| 38 | source: Attachment, | |
| 39 | layout: Box<crate::outline::ObjectLayout>, | |
| 40 | }, | |
| 36 | 41 | Ink(Ink), |
| 37 | 42 | Editable(onestore::ExGuid), |
| 38 | 43 | ReadOnly(Box<ReadOnlyObject>), |
| ... | ... | @@ -60,22 +65,46 @@ impl Content { |
| 60 | 65 | )?)) |
| 61 | 66 | } |
| 62 | 67 | |
| 68 | /// The page-level picture shown, drawn or as a placeholder. | |
| 69 | pub(super) fn picture(&self) -> Option<&Image> { | |
| 70 | match self { | |
| 71 | Self::Image(image) => Some(image), | |
| 72 | Self::ReadOnly(object) => match &object.source { | |
| 73 | PageObject::Image(image) => Some(image), | |
| 74 | _ => None, | |
| 75 | }, | |
| 76 | _ => None, | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 63 | 80 | pub(super) fn layout(&self) -> Option<(onestore::ExGuid, &onestore::document::Layout)> { |
| 64 | 81 | match self { |
| 65 | 82 | Self::Outline { source, .. } => Some((source.id, &source.layout)), |
| 66 | 83 | Self::Image(source) => Some((source.id, &source.layout)), |
| 84 | Self::File { source, .. } => Some((source.id, &source.layout)), | |
| 67 | 85 | Self::Ink(source) => Some((source.id, &source.layout)), |
| 68 | 86 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout())), |
| 69 | 87 | Self::Date { .. } | Self::Editable(_) => None, |
| 70 | 88 | } |
| 71 | 89 | } |
| 72 | 90 | |
| 91 | /// A file on the page, where its column's `[x0, y0, x1, y1]` lie in page points. | |
| 92 | pub(crate) fn file(&self) -> Option<(&Attachment, [f32; 4])> { | |
| 93 | let Self::File { source, layout } = self else { | |
| 94 | return None; | |
| 95 | }; | |
| 96 | let [x, y] = [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0)); | |
| 97 | let [x0, y0, x1, y1] = layout.bounds(); | |
| 98 | Some((source, [x + x0, y + y0, x + x1, y + y1])) | |
| 99 | } | |
| 100 | ||
| 73 | 101 | pub(super) fn layout_mut( |
| 74 | 102 | &mut self, |
| 75 | 103 | ) -> Option<(onestore::ExGuid, &mut onestore::document::Layout)> { |
| 76 | 104 | match self { |
| 77 | 105 | Self::Outline { source, .. } => Some((source.id, &mut source.layout)), |
| 78 | 106 | Self::Image(source) => Some((source.id, &mut source.layout)), |
| 107 | Self::File { source, .. } => Some((source.id, &mut source.layout)), | |
| 79 | 108 | Self::Ink(source) => Some((source.id, &mut source.layout)), |
| 80 | 109 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())), |
| 81 | 110 | Self::Date { .. } | Self::Editable(_) => None, |
| ... | ... | @@ -332,6 +361,19 @@ pub(crate) fn build( |
| 332 | 361 | }; |
| 333 | 362 | objects.push(Content::Image(source)); |
| 334 | 363 | } |
| 364 | PageObject::Attachment(source) => { | |
| 365 | if [source.layout.x, source.layout.y] | |
| 366 | .iter() | |
| 367 | .any(|v| !v.unwrap_or(0.0).is_finite()) | |
| 368 | { | |
| 369 | return Err(EditorError::InvalidGeometry); | |
| 370 | } | |
| 371 | let layout = Box::new(crate::outline::page_file(engine, source)?); | |
| 372 | let PageObject::Attachment(source) = object else { | |
| 373 | unreachable!() | |
| 374 | }; | |
| 375 | objects.push(Content::File { source, layout }); | |
| 376 | } | |
| 335 | 377 | PageObject::Ink(ink) => { |
| 336 | 378 | if ink_bounds(ink).is_some_and(|b| b.iter().any(|v| !v.is_finite())) { |
| 337 | 379 | return Err(EditorError::InvalidGeometry); |
crates/canvas/src/editor/space.rs+2-1| ... | ... | @@ -5,7 +5,7 @@ use super::*; |
| 5 | 5 | |
| 6 | 6 | /// Something Insert Space moves. |
| 7 | 7 | enum Piece { |
| 8 | /// A body outline, picture or ink drawing, placed anew. | |
| 8 | /// A body outline, picture, file or ink drawing, placed anew. | |
| 9 | 9 | Object(ExGuid), |
| 10 | 10 | /// The root nodes of a body outline from `at` on. |
| 11 | 11 | Tail { outline: ExGuid, at: usize }, |
| ... | ... | @@ -81,6 +81,7 @@ impl CanvasEditor { |
| 81 | 81 | ]; |
| 82 | 82 | (image.id, rect) |
| 83 | 83 | } |
| 84 | page::Content::File { source, .. } => (source.id, content.file().unwrap().1), | |
| 84 | 85 | page::Content::Ink(ink) => match page::ink_bounds(ink) { |
| 85 | 86 | Some(rect) => (ink.id, rect), |
| 86 | 87 | None => continue, |
crates/canvas/src/gpu/mod.rs+206-126| ... | ... | @@ -4,7 +4,7 @@ mod profile; |
| 4 | 4 | |
| 5 | 5 | use crate::{ |
| 6 | 6 | layout::{TextBrush, TextLayout}, |
| 7 | outline::{BoxMark, TagIcon}, | |
| 7 | outline::TagIcon, | |
| 8 | 8 | }; |
| 9 | 9 | use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError}; |
| 10 | 10 | use parley::PositionedLayoutItem; |
| ... | ... | @@ -140,116 +140,199 @@ pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout { |
| 140 | 140 | .expect("Page text is a TextLayout") |
| 141 | 141 | } |
| 142 | 142 | |
| 143 | const CHECKBOX: &str = include_str!("../../assets/tags/checkbox.svg"); | |
| 144 | 143 | const CHECKMARK: &str = include_str!("../../assets/tags/checkmark.svg"); |
| 145 | const STAR: &str = include_str!("../../assets/tags/star.svg"); | |
| 146 | const QUESTION: &str = include_str!("../../assets/tags/question.svg"); | |
| 147 | const HIGHLIGHT: &str = include_str!("../../assets/tags/highlight.svg"); | |
| 148 | const CONTACT: &str = include_str!("../../assets/tags/contact.svg"); | |
| 149 | const ADDRESS: &str = include_str!("../../assets/tags/address.svg"); | |
| 150 | const PHONE: &str = include_str!("../../assets/tags/phone.svg"); | |
| 151 | const MUSIC: &str = include_str!("../../assets/tags/music.svg"); | |
| 152 | const EXCLAMATION: &str = include_str!("../../assets/tags/exclamation.svg"); | |
| 153 | const RED_SQUARE: &str = include_str!("../../assets/tags/red-square.svg"); | |
| 154 | const YELLOW_SQUARE: &str = include_str!("../../assets/tags/yellow-square.svg"); | |
| 155 | const BLUE_SQUARE: &str = include_str!("../../assets/tags/blue-square.svg"); | |
| 156 | 144 | const FLAG: &str = include_str!("../../assets/tags/flag.svg"); |
| 157 | const IDEA: &str = include_str!("../../assets/tags/idea.svg"); | |
| 158 | const PASSWORD: &str = include_str!("../../assets/tags/password.svg"); | |
| 159 | const MOVIE: &str = include_str!("../../assets/tags/movie.svg"); | |
| 160 | const BOOK: &str = include_str!("../../assets/tags/book.svg"); | |
| 161 | const WEB: &str = include_str!("../../assets/tags/web.svg"); | |
| 162 | const BLOG: &str = include_str!("../../assets/tags/blog.svg"); | |
| 163 | const EMAIL: &str = include_str!("../../assets/tags/email.svg"); | |
| 164 | 145 | const BOX_SMALL: &str = include_str!("../../assets/tags/box-small.svg"); |
| 165 | 146 | const CHECK_SMALL: &str = include_str!("../../assets/tags/check-small.svg"); |
| 166 | const MARK_PERSON: &str = include_str!("../../assets/tags/mark-person.svg"); | |
| 167 | const MARK_MANAGER: &str = include_str!("../../assets/tags/mark-manager.svg"); | |
| 168 | const MARK_ARROW: &str = include_str!("../../assets/tags/mark-arrow.svg"); | |
| 169 | const MARK_ONE: &str = include_str!("../../assets/tags/mark-one.svg"); | |
| 170 | const MARK_TWO: &str = include_str!("../../assets/tags/mark-two.svg"); | |
| 171 | const MARK_CLIENT: &str = include_str!("../../assets/tags/mark-client.svg"); | |
| 172 | 147 | const TAG: &str = include_str!("../../assets/tags/tag.svg"); |
| 173 | 148 | |
| 149 | /// How a tag symbol's artwork takes a check: a large box under it, a small box with the | |
| 150 | /// check between the box and its badge, or art that shows none. | |
| 151 | enum Art { | |
| 152 | Box(&'static str), | |
| 153 | Badged(&'static str), | |
| 154 | Plain(&'static str), | |
| 155 | } | |
| 156 | ||
| 157 | /// The artwork of symbol `shape` of MS-ONE's NoteTagShape, as OneNote 2010 draws it on the | |
| 158 | /// page: it draws 54, Green 8-Point Star, blue there. A shape MS-ONE does not list shows a | |
| 159 | /// plain tag. | |
| 160 | fn art(shape: u16) -> Art { | |
| 161 | use Art::{Badged, Box, Plain}; | |
| 162 | macro_rules! tag { | |
| 163 | ($name:literal) => { | |
| 164 | include_str!(concat!("../../assets/tags/", $name, ".svg")) | |
| 165 | }; | |
| 166 | } | |
| 167 | match shape { | |
| 168 | 1 => Box(tag!("checkbox-green")), | |
| 169 | 2 => Box(tag!("checkbox-yellow")), | |
| 170 | 3 => Box(tag!("checkbox")), | |
| 171 | 4 => Badged(tag!("mark-star-green")), | |
| 172 | 5 => Badged(tag!("mark-star-yellow")), | |
| 173 | 6 => Badged(tag!("mark-star-blue")), | |
| 174 | 7 => Badged(tag!("mark-exclamation-green")), | |
| 175 | 8 => Badged(tag!("mark-exclamation-yellow")), | |
| 176 | 9 => Badged(tag!("mark-exclamation-blue")), | |
| 177 | 10 => Badged(tag!("mark-arrow-green")), | |
| 178 | 11 => Badged(tag!("mark-arrow-yellow")), | |
| 179 | 12 => Badged(tag!("mark-arrow-blue")), | |
| 180 | 13 => Plain(tag!("star")), | |
| 181 | 14 => Plain(tag!("follow-up")), | |
| 182 | 15 => Plain(tag!("question")), | |
| 183 | 16 => Plain(tag!("arrow-right-blue")), | |
| 184 | 17 => Plain(tag!("exclamation")), | |
| 185 | 18 => Plain(tag!("phone")), | |
| 186 | 19 => Plain(tag!("calendar")), | |
| 187 | 20 => Plain(tag!("clock")), | |
| 188 | 21 => Plain(tag!("idea")), | |
| 189 | 22 => Plain(tag!("pushpin")), | |
| 190 | 23 => Plain(tag!("address")), | |
| 191 | 24 => Plain(tag!("blog")), | |
| 192 | 25 => Plain(tag!("smiley")), | |
| 193 | 26 => Plain(tag!("ribbon")), | |
| 194 | 27 => Plain(tag!("key")), | |
| 195 | 28 => Badged(tag!("mark-one-blue")), | |
| 196 | 29 => Plain(tag!("circle-1-blue")), | |
| 197 | 30 => Badged(tag!("mark-two-blue")), | |
| 198 | 31 => Plain(tag!("circle-2-blue")), | |
| 199 | 32 => Badged(tag!("mark-three-blue")), | |
| 200 | 33 => Plain(tag!("circle-3-blue")), | |
| 201 | 34 => Plain(tag!("star8-blue")), | |
| 202 | 35 => Plain(tag!("tick-blue")), | |
| 203 | 36 => Plain(tag!("circle-blue")), | |
| 204 | 37 => Plain(tag!("arrow-down-blue")), | |
| 205 | 38 => Plain(tag!("arrow-left-blue")), | |
| 206 | 39 => Plain(tag!("solid-target-blue")), | |
| 207 | 40 => Plain(tag!("star-blue")), | |
| 208 | 41 => Plain(tag!("sun-blue")), | |
| 209 | 42 => Plain(tag!("target-blue")), | |
| 210 | 43 => Plain(tag!("triangle-blue")), | |
| 211 | 44 => Plain(tag!("umbrella-blue")), | |
| 212 | 45 => Plain(tag!("arrow-up-blue")), | |
| 213 | 46 => Plain(tag!("x-dots-blue")), | |
| 214 | 47 => Plain(tag!("x-blue")), | |
| 215 | 48 => Badged(tag!("mark-one-green")), | |
| 216 | 49 => Plain(tag!("circle-1-green")), | |
| 217 | 50 => Badged(tag!("mark-two-green")), | |
| 218 | 51 => Plain(tag!("circle-2-green")), | |
| 219 | 52 => Badged(tag!("mark-three-green")), | |
| 220 | 53 => Plain(tag!("circle-3-green")), | |
| 221 | 54 => Plain(tag!("star8-blue")), | |
| 222 | 55 => Plain(tag!("tick-green")), | |
| 223 | 56 => Plain(tag!("circle-green")), | |
| 224 | 57 => Plain(tag!("arrow-down-green")), | |
| 225 | 58 => Plain(tag!("arrow-left-green")), | |
| 226 | 59 => Plain(tag!("arrow-right-green")), | |
| 227 | 60 => Plain(tag!("solid-target-green")), | |
| 228 | 61 => Plain(tag!("star-green")), | |
| 229 | 62 => Plain(tag!("sun-green")), | |
| 230 | 63 => Plain(tag!("target-green")), | |
| 231 | 64 => Plain(tag!("triangle-green")), | |
| 232 | 65 => Plain(tag!("umbrella-green")), | |
| 233 | 66 => Plain(tag!("arrow-up-green")), | |
| 234 | 67 => Plain(tag!("x-dots-green")), | |
| 235 | 68 => Plain(tag!("x-green")), | |
| 236 | 69 => Badged(tag!("mark-one-yellow")), | |
| 237 | 70 => Plain(tag!("circle-1-yellow")), | |
| 238 | 71 => Badged(tag!("mark-two-yellow")), | |
| 239 | 72 => Plain(tag!("circle-2-yellow")), | |
| 240 | 73 => Badged(tag!("mark-three-yellow")), | |
| 241 | 74 => Plain(tag!("circle-3-yellow")), | |
| 242 | 75 => Plain(tag!("star8-yellow")), | |
| 243 | 76 => Plain(tag!("tick-yellow")), | |
| 244 | 77 => Plain(tag!("circle-yellow")), | |
| 245 | 78 => Plain(tag!("arrow-down-yellow")), | |
| 246 | 79 => Plain(tag!("arrow-left-yellow")), | |
| 247 | 80 => Plain(tag!("arrow-right-yellow")), | |
| 248 | 81 => Plain(tag!("solid-target-yellow")), | |
| 249 | 82 => Plain(tag!("sun-yellow")), | |
| 250 | 83 => Plain(tag!("target-yellow")), | |
| 251 | 84 => Plain(tag!("triangle-yellow")), | |
| 252 | 85 => Plain(tag!("umbrella-yellow")), | |
| 253 | 86 => Plain(tag!("arrow-up-yellow")), | |
| 254 | 87 => Plain(tag!("x-dots-yellow")), | |
| 255 | 88 => Plain(tag!("x-yellow")), | |
| 256 | 89 => Plain(tag!("flag-today")), | |
| 257 | 90 => Plain(tag!("flag-tomorrow")), | |
| 258 | 91 => Plain(tag!("flag-this-week")), | |
| 259 | 92 => Plain(tag!("flag-next-week")), | |
| 260 | 93 => Plain(tag!("flag-no-date")), | |
| 261 | 94 => Badged(tag!("mark-person-blue")), | |
| 262 | 95 => Badged(tag!("mark-person-yellow")), | |
| 263 | 96 => Badged(tag!("mark-person-green")), | |
| 264 | 97 => Badged(tag!("mark-flag-blue")), | |
| 265 | 98 => Badged(tag!("mark-flag-yellow")), | |
| 266 | 99 => Badged(tag!("mark-flag-green")), | |
| 267 | 100 => Plain(tag!("red-square")), | |
| 268 | 101 => Plain(tag!("yellow-square")), | |
| 269 | 102 => Plain(tag!("blue-square")), | |
| 270 | 103 => Plain(tag!("green-square")), | |
| 271 | 104 => Plain(tag!("orange-square")), | |
| 272 | 105 => Plain(tag!("pink-square")), | |
| 273 | 106 => Plain(tag!("email")), | |
| 274 | 107 => Plain(tag!("envelope")), | |
| 275 | 108 => Plain(tag!("envelope-open")), | |
| 276 | 109 => Plain(tag!("mobile")), | |
| 277 | 110 => Plain(tag!("phone-clock")), | |
| 278 | 111 => Plain(tag!("question-balloon")), | |
| 279 | 112 => Plain(tag!("paperclip")), | |
| 280 | 113 => Plain(tag!("frown")), | |
| 281 | 114 => Plain(tag!("im-contact")), | |
| 282 | 115 => Plain(tag!("person")), | |
| 283 | 116 => Plain(tag!("people")), | |
| 284 | 117 => Plain(tag!("bell")), | |
| 285 | 118 => Plain(tag!("contact")), | |
| 286 | 119 => Plain(tag!("rose")), | |
| 287 | 120 => Plain(tag!("date")), | |
| 288 | 121 => Plain(tag!("music")), | |
| 289 | 122 => Plain(tag!("movie")), | |
| 290 | 123 => Plain(tag!("quote")), | |
| 291 | 124 => Plain(tag!("globe")), | |
| 292 | 125 => Plain(tag!("web")), | |
| 293 | 126 => Plain(tag!("laptop")), | |
| 294 | 127 => Plain(tag!("plane")), | |
| 295 | 128 => Plain(tag!("car")), | |
| 296 | 129 => Plain(tag!("binoculars")), | |
| 297 | 130 => Plain(tag!("presentation")), | |
| 298 | 131 => Plain(tag!("password")), | |
| 299 | 132 => Plain(tag!("book")), | |
| 300 | 133 => Plain(tag!("notebook")), | |
| 301 | 134 => Plain(tag!("paper")), | |
| 302 | 135 => Plain(tag!("research")), | |
| 303 | 136 => Plain(tag!("highlight")), | |
| 304 | 137 => Plain(tag!("dollar")), | |
| 305 | 138 => Plain(tag!("coins")), | |
| 306 | 139 => Plain(tag!("schedule")), | |
| 307 | 140 => Plain(tag!("lightning")), | |
| 308 | 141 => Plain(tag!("cloud")), | |
| 309 | 142 => Plain(tag!("heart")), | |
| 310 | 143 => Plain(tag!("sunflower")), | |
| 311 | _ => Plain(TAG), | |
| 312 | } | |
| 313 | } | |
| 314 | ||
| 315 | /// Symbols MS-ONE lists. | |
| 316 | const SYMBOLS: u16 = 143; | |
| 317 | ||
| 174 | 318 | /// The tag's artwork, drawn in order. |
| 175 | 319 | pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] { |
| 176 | match icon { | |
| 177 | TagIcon::CheckBox { | |
| 178 | checked: false, | |
| 179 | mark: None, | |
| 180 | } => &[CHECKBOX], | |
| 181 | TagIcon::CheckBox { | |
| 182 | checked: true, | |
| 183 | mark: None, | |
| 184 | } => &[CHECKBOX, CHECKMARK], | |
| 185 | TagIcon::CheckBox { | |
| 186 | checked: false, | |
| 187 | mark: Some(BoxMark::Person), | |
| 188 | } => &[BOX_SMALL, MARK_PERSON], | |
| 189 | TagIcon::CheckBox { | |
| 190 | checked: true, | |
| 191 | mark: Some(BoxMark::Person), | |
| 192 | } => &[BOX_SMALL, CHECK_SMALL, MARK_PERSON], | |
| 193 | TagIcon::CheckBox { | |
| 194 | checked: false, | |
| 195 | mark: Some(BoxMark::Manager), | |
| 196 | } => &[BOX_SMALL, MARK_MANAGER], | |
| 197 | TagIcon::CheckBox { | |
| 198 | checked: true, | |
| 199 | mark: Some(BoxMark::Manager), | |
| 200 | } => &[BOX_SMALL, CHECK_SMALL, MARK_MANAGER], | |
| 201 | TagIcon::CheckBox { | |
| 202 | checked: false, | |
| 203 | mark: Some(BoxMark::Arrow), | |
| 204 | } => &[BOX_SMALL, MARK_ARROW], | |
| 205 | TagIcon::CheckBox { | |
| 206 | checked: true, | |
| 207 | mark: Some(BoxMark::Arrow), | |
| 208 | } => &[BOX_SMALL, CHECK_SMALL, MARK_ARROW], | |
| 209 | TagIcon::CheckBox { | |
| 210 | checked: false, | |
| 211 | mark: Some(BoxMark::One), | |
| 212 | } => &[BOX_SMALL, MARK_ONE], | |
| 213 | TagIcon::CheckBox { | |
| 214 | checked: true, | |
| 215 | mark: Some(BoxMark::One), | |
| 216 | } => &[BOX_SMALL, CHECK_SMALL, MARK_ONE], | |
| 217 | TagIcon::CheckBox { | |
| 218 | checked: false, | |
| 219 | mark: Some(BoxMark::Two), | |
| 220 | } => &[BOX_SMALL, MARK_TWO], | |
| 221 | TagIcon::CheckBox { | |
| 222 | checked: true, | |
| 223 | mark: Some(BoxMark::Two), | |
| 224 | } => &[BOX_SMALL, CHECK_SMALL, MARK_TWO], | |
| 225 | TagIcon::CheckBox { | |
| 226 | checked: false, | |
| 227 | mark: Some(BoxMark::Client), | |
| 228 | } => &[BOX_SMALL, MARK_CLIENT], | |
| 229 | TagIcon::CheckBox { | |
| 230 | checked: true, | |
| 231 | mark: Some(BoxMark::Client), | |
| 232 | } => &[BOX_SMALL, CHECK_SMALL, MARK_CLIENT], | |
| 233 | TagIcon::Star => &[STAR], | |
| 234 | TagIcon::Question => &[QUESTION], | |
| 235 | TagIcon::Highlight => &[HIGHLIGHT], | |
| 236 | TagIcon::Contact => &[CONTACT], | |
| 237 | TagIcon::Address => &[ADDRESS], | |
| 238 | TagIcon::Phone => &[PHONE], | |
| 239 | TagIcon::Music => &[MUSIC], | |
| 240 | TagIcon::Exclamation => &[EXCLAMATION], | |
| 241 | TagIcon::RedSquare => &[RED_SQUARE], | |
| 242 | TagIcon::YellowSquare => &[YELLOW_SQUARE], | |
| 243 | TagIcon::BlueSquare => &[BLUE_SQUARE], | |
| 244 | TagIcon::Flag => &[FLAG], | |
| 245 | TagIcon::Idea => &[IDEA], | |
| 246 | TagIcon::Password => &[PASSWORD], | |
| 247 | TagIcon::Movie => &[MOVIE], | |
| 248 | TagIcon::Book => &[BOOK], | |
| 249 | TagIcon::Web => &[WEB], | |
| 250 | TagIcon::Blog => &[BLOG], | |
| 251 | TagIcon::Email => &[EMAIL], | |
| 252 | TagIcon::Other => &[TAG], | |
| 320 | static SOURCES: std::sync::OnceLock<Vec<[Vec<&'static str>; 2]>> = std::sync::OnceLock::new(); | |
| 321 | let TagIcon::Symbol { shape, checked } = icon else { | |
| 322 | return &[FLAG]; | |
| 323 | }; | |
| 324 | let sources = SOURCES.get_or_init(|| { | |
| 325 | (0..=SYMBOLS) | |
| 326 | .map(|shape| match art(shape) { | |
| 327 | Art::Box(art) => [vec![art], vec![art, CHECKMARK]], | |
| 328 | Art::Badged(badge) => [vec![BOX_SMALL, badge], vec![BOX_SMALL, CHECK_SMALL, badge]], | |
| 329 | Art::Plain(art) => [vec![art], vec![art]], | |
| 330 | }) | |
| 331 | .collect() | |
| 332 | }); | |
| 333 | match sources.get(usize::from(shape)) { | |
| 334 | Some(sources) => &sources[usize::from(checked)], | |
| 335 | None => &[TAG], | |
| 253 | 336 | } |
| 254 | 337 | } |
| 255 | 338 | |
| ... | ... | @@ -364,29 +447,26 @@ mod tests { |
| 364 | 447 | mapped_at_creation: false, |
| 365 | 448 | }); |
| 366 | 449 | let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); |
| 367 | let icons = [ | |
| 368 | TagIcon::CheckBox { | |
| 369 | checked: false, | |
| 370 | mark: None, | |
| 371 | }, | |
| 372 | TagIcon::CheckBox { | |
| 373 | checked: true, | |
| 374 | mark: None, | |
| 375 | }, | |
| 376 | TagIcon::Star, | |
| 377 | TagIcon::Question, | |
| 378 | TagIcon::Highlight, | |
| 379 | TagIcon::Contact, | |
| 380 | TagIcon::Address, | |
| 381 | TagIcon::Phone, | |
| 382 | TagIcon::Music, | |
| 383 | TagIcon::Exclamation, | |
| 384 | TagIcon::RedSquare, | |
| 385 | TagIcon::YellowSquare, | |
| 386 | TagIcon::BlueSquare, | |
| 387 | TagIcon::Flag, | |
| 388 | TagIcon::Other, | |
| 389 | ]; | |
| 450 | let icons: Vec<_> = [ | |
| 451 | (3, false), | |
| 452 | (3, true), | |
| 453 | (13, false), | |
| 454 | (15, false), | |
| 455 | (136, false), | |
| 456 | (118, false), | |
| 457 | (23, false), | |
| 458 | (18, false), | |
| 459 | (121, false), | |
| 460 | (17, false), | |
| 461 | (100, false), | |
| 462 | (101, false), | |
| 463 | (102, false), | |
| 464 | (999, false), | |
| 465 | ] | |
| 466 | .into_iter() | |
| 467 | .map(|(shape, checked)| TagIcon::of(shape, checked).unwrap()) | |
| 468 | .chain([TagIcon::Task]) | |
| 469 | .collect(); | |
| 390 | 470 | let text = TextEngine::default() |
| 391 | 471 | .layout( |
| 392 | 472 | &Paragraph::from_runs([ |
crates/canvas/src/gpu/page.rs+139-39| ... | ... | @@ -79,6 +79,7 @@ pub enum SceneHit<T> { |
| 79 | 79 | Date(DateField), |
| 80 | 80 | ReadOnly(usize), |
| 81 | 81 | Image(onestore::ExGuid), |
| 82 | File(onestore::ExGuid), | |
| 82 | 83 | } |
| 83 | 84 | |
| 84 | 85 | fn outline_origin( |
| ... | ... | @@ -139,6 +140,14 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec< |
| 139 | 140 | } |
| 140 | 141 | } |
| 141 | 142 | |
| 143 | const FILE_ICON: &str = include_str!("../../assets/file.svg"); | |
| 144 | ||
| 145 | /// The blank page for a file without an icon of its own, as the 32 pixel PNG OneNote 2010 | |
| 146 | /// stores with every attachment; it draws a broken picture for one stored without. | |
| 147 | pub fn file_icon() -> Vec<u8> { | |
| 148 | background::png(FILE_ICON, 32) | |
| 149 | } | |
| 150 | ||
| 142 | 151 | fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) { |
| 143 | 152 | primitives.push(Primitive::Rect { |
| 144 | 153 | rect, |
| ... | ... | @@ -230,10 +239,9 @@ impl PageScene { |
| 230 | 239 | } |
| 231 | 240 | } |
| 232 | 241 | for (id, art) in &mut scene.backgrounds { |
| 233 | if let Some(shown) = self.backgrounds.remove(id) | |
| 234 | && shown.same(art) | |
| 235 | { | |
| 236 | *art = shown; | |
| 242 | match self.backgrounds.remove(id) { | |
| 243 | Some(shown) if shown.same(art) => *art = shown, | |
| 244 | _ => art.inline = true, | |
| 237 | 245 | } |
| 238 | 246 | } |
| 239 | 247 | scene.mark_unavailable(&mut editor.objects, engine)?; |
| ... | ... | @@ -241,6 +249,28 @@ impl PageScene { |
| 241 | 249 | Ok(()) |
| 242 | 250 | } |
| 243 | 251 | |
| 252 | /// Follows `editor` after an edit or undo brought page-level pictures the scene has not | |
| 253 | /// seen, as `refresh` does. | |
| 254 | pub fn follow( | |
| 255 | &mut self, | |
| 256 | editor: &mut CanvasEditor, | |
| 257 | engine: &mut TextEngine, | |
| 258 | ) -> Result<(), SceneError> { | |
| 259 | let known = |object: &Content| match object { | |
| 260 | Content::Image(source) => { | |
| 261 | self.pictures.contains_key(&source.id) || self.backgrounds.contains_key(&source.id) | |
| 262 | } | |
| 263 | Content::File { source, .. } => { | |
| 264 | source.preview.is_none() || self.pictures.contains_key(&source.id) | |
| 265 | } | |
| 266 | _ => true, | |
| 267 | }; | |
| 268 | if editor.objects.iter().all(known) { | |
| 269 | return Ok(()); | |
| 270 | } | |
| 271 | self.refresh(editor, engine) | |
| 272 | } | |
| 273 | ||
| 244 | 274 | /// Turns page pictures the renderer cannot decode into placeholders; their stored data |
| 245 | 275 | /// is kept. |
| 246 | 276 | fn mark_unavailable( |
| ... | ... | @@ -275,7 +305,6 @@ impl PageScene { |
| 275 | 305 | onestore::page::ParagraphContent::Image(image) => { |
| 276 | 306 | payloads.push((image.id, image.bytes.as_ref())) |
| 277 | 307 | } |
| 278 | // A file without the icon OneNote rendered for it keeps an empty slot. | |
| 279 | 308 | onestore::page::ParagraphContent::Attachment(file) => { |
| 280 | 309 | if let Some(icon) = file.preview.as_ref() { |
| 281 | 310 | payloads.push((file.id, Some(icon))) |
| ... | ... | @@ -303,6 +332,11 @@ impl PageScene { |
| 303 | 332 | payloads.push((source.id, source.bytes.as_ref())) |
| 304 | 333 | } |
| 305 | 334 | } |
| 335 | Content::File { source, .. } => { | |
| 336 | if let Some(icon) = source.preview.as_ref() { | |
| 337 | payloads.push((source.id, Some(icon))) | |
| 338 | } | |
| 339 | } | |
| 306 | 340 | Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads), |
| 307 | 341 | Content::Editable(id) => { |
| 308 | 342 | if let Some(outline) = |
| ... | ... | @@ -396,6 +430,11 @@ impl PageScene { |
| 396 | 430 | rects.push((source.id, [x, y, x + width, y + height])); |
| 397 | 431 | } |
| 398 | 432 | } |
| 433 | Content::File { source, layout } => { | |
| 434 | let [x, y] = [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0)); | |
| 435 | let [x0, y0, x1, y1] = layout.rect; | |
| 436 | rects.push((source.id, [x + x0, y + y0, x + x1, y + y1])); | |
| 437 | } | |
| 399 | 438 | Content::Outline { |
| 400 | 439 | source, |
| 401 | 440 | layout, |
| ... | ... | @@ -433,40 +472,61 @@ impl PageScene { |
| 433 | 472 | primitives: &mut Vec<Primitive<'a>>, |
| 434 | 473 | ) { |
| 435 | 474 | for object in &outline.objects { |
| 436 | let [x0, y0, x1, y1] = object.rect; | |
| 437 | let rect = match moving { | |
| 438 | Some((id, rect)) if id == object.id => rect, | |
| 439 | _ => [ | |
| 440 | x0 + origin[0], | |
| 441 | y0 + origin[1], | |
| 442 | x1 + origin[0], | |
| 443 | y1 + origin[1], | |
| 444 | ], | |
| 445 | }; | |
| 446 | let picture = self.pictures.get(&object.id); | |
| 447 | if let Some(image) = picture.and_then(Picture::image) { | |
| 448 | primitives.push(Primitive::Image { image, rect }); | |
| 475 | self.append_object(object, origin, moving, paper, primitives); | |
| 476 | } | |
| 477 | } | |
| 478 | ||
| 479 | /// One picture, file, drawing or placeholder laid out from `origin`; `moving` as for | |
| 480 | /// `append_outline_objects`. | |
| 481 | fn append_object<'a>( | |
| 482 | &'a self, | |
| 483 | object: &'a crate::outline::ObjectLayout, | |
| 484 | origin: [f32; 2], | |
| 485 | moving: Option<(onestore::ExGuid, [f32; 4])>, | |
| 486 | paper: Paper, | |
| 487 | primitives: &mut Vec<Primitive<'a>>, | |
| 488 | ) { | |
| 489 | let [x0, y0, x1, y1] = object.rect; | |
| 490 | let rect = match moving { | |
| 491 | Some((id, rect)) if id == object.id => rect, | |
| 492 | _ => [ | |
| 493 | x0 + origin[0], | |
| 494 | y0 + origin[1], | |
| 495 | x1 + origin[0], | |
| 496 | y1 + origin[1], | |
| 497 | ], | |
| 498 | }; | |
| 499 | let picture = self.pictures.get(&object.id); | |
| 500 | if let Some(image) = picture.and_then(Picture::image) { | |
| 501 | primitives.push(Primitive::Image { image, rect }); | |
| 502 | } | |
| 503 | match &object.kind { | |
| 504 | crate::outline::ObjectKind::Ink(ink) => append_ink(ink, [rect[0], rect[1]], primitives), | |
| 505 | crate::outline::ObjectKind::Unsupported(_) => { | |
| 506 | append_placeholder(rect, paper, primitives) | |
| 449 | 507 | } |
| 450 | match &object.kind { | |
| 451 | crate::outline::ObjectKind::Ink(ink) => { | |
| 452 | append_ink(ink, [rect[0], rect[1]], primitives) | |
| 453 | } | |
| 454 | crate::outline::ObjectKind::Unsupported(_) => { | |
| 455 | append_placeholder(rect, paper, primitives) | |
| 456 | } | |
| 457 | crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => { | |
| 458 | append_placeholder(rect, paper, primitives) | |
| 459 | } | |
| 460 | crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {} | |
| 508 | crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => { | |
| 509 | append_placeholder(rect, paper, primitives) | |
| 461 | 510 | } |
| 462 | if let Some(label) = object.label() { | |
| 463 | primitives.push(Primitive::Text { | |
| 464 | text: &label.text, | |
| 465 | origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]], | |
| 466 | clip: None, | |
| 467 | ink: paper.ink, | |
| 468 | }); | |
| 511 | // A file stored without the icon OneNote rendered for it shows a blank page. | |
| 512 | crate::outline::ObjectKind::File(_) if picture.is_none() => { | |
| 513 | primitives.push(Primitive::Icon { | |
| 514 | sources: &[FILE_ICON], | |
| 515 | origin: [rect[0], rect[1]], | |
| 516 | size: rect[2] - rect[0], | |
| 517 | tint: [1.0; 4], | |
| 518 | palette: draw::Palette::default(), | |
| 519 | }) | |
| 469 | 520 | } |
| 521 | crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {} | |
| 522 | } | |
| 523 | if let Some(label) = object.label() { | |
| 524 | primitives.push(Primitive::Text { | |
| 525 | text: &label.text, | |
| 526 | origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]], | |
| 527 | clip: None, | |
| 528 | ink: paper.ink, | |
| 529 | }); | |
| 470 | 530 | } |
| 471 | 531 | } |
| 472 | 532 | |
| ... | ... | @@ -558,6 +618,7 @@ impl PageScene { |
| 558 | 618 | ], |
| 559 | 619 | false, |
| 560 | 620 | )), |
| 621 | Content::File { .. } => Some((object.file()?.1, true)), | |
| 561 | 622 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), |
| 562 | 623 | Content::ReadOnly(object) => Some((object.rect(), true)), |
| 563 | 624 | Content::Editable(_) => None, |
| ... | ... | @@ -652,6 +713,12 @@ impl PageScene { |
| 652 | 713 | return Some(SceneHit::Image(source.id)); |
| 653 | 714 | } |
| 654 | 715 | } |
| 716 | Content::File { .. } => { | |
| 717 | let (file, [x0, y0, x1, y1]) = object.file()?; | |
| 718 | if (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]) { | |
| 719 | return Some(SceneHit::File(file.id)); | |
| 720 | } | |
| 721 | } | |
| 655 | 722 | Content::Outline { .. } | Content::Image(_) | Content::Ink(_) => {} |
| 656 | 723 | } |
| 657 | 724 | } |
| ... | ... | @@ -737,13 +804,43 @@ impl PageScene { |
| 737 | 804 | append_ink(ink, [offset[0] + x, offset[1] + y], primitives); |
| 738 | 805 | continue; |
| 739 | 806 | } |
| 807 | Content::File { source, layout } => { | |
| 808 | // A dragged file draws where its column would land. | |
| 809 | let [x, y] = match moving { | |
| 810 | Some((id, [x0, y0, ..])) if id == source.id => [x0, y0], | |
| 811 | _ => [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0)), | |
| 812 | }; | |
| 813 | self.append_object( | |
| 814 | layout, | |
| 815 | [x + offset[0], y + offset[1]], | |
| 816 | None, | |
| 817 | paper, | |
| 818 | primitives, | |
| 819 | ); | |
| 820 | continue; | |
| 821 | } | |
| 740 | 822 | }; |
| 741 | 823 | let object_origin = [origin[0] + offset[0], origin[1] + offset[1]]; |
| 742 | 824 | match content { |
| 743 | 825 | Content::Image(source) => { |
| 744 | // The paper shows until the worker's raster lands. | |
| 745 | 826 | let (image, [width, height]) = match self.backgrounds.get(&source.id) { |
| 746 | Some(art) => (art.image(paper), art.size), | |
| 827 | Some(art) => { | |
| 828 | // OneNote's art is opaque, over white, and hides the rule lines. | |
| 829 | primitives.push(Primitive::Rect { | |
| 830 | rect: [ | |
| 831 | object_origin[0], | |
| 832 | object_origin[1], | |
| 833 | object_origin[0] + art.size[0], | |
| 834 | object_origin[1] + art.size[1], | |
| 835 | ], | |
| 836 | color: if background::dark(paper) { | |
| 837 | paper.color | |
| 838 | } else { | |
| 839 | Paper::WHITE.color | |
| 840 | }, | |
| 841 | }); | |
| 842 | (art.image(paper), art.size) | |
| 843 | } | |
| 747 | 844 | None => { |
| 748 | 845 | let picture = self |
| 749 | 846 | .pictures |
| ... | ... | @@ -816,7 +913,10 @@ impl PageScene { |
| 816 | 913 | ); |
| 817 | 914 | } |
| 818 | 915 | } |
| 819 | Content::Editable(_) | Content::ReadOnly(_) | Content::Ink(_) => unreachable!(), | |
| 916 | Content::Editable(_) | |
| 917 | | Content::ReadOnly(_) | |
| 918 | | Content::Ink(_) | |
| 919 | | Content::File { .. } => unreachable!(), | |
| 820 | 920 | } |
| 821 | 921 | } |
| 822 | 922 | Ok(()) |
crates/canvas/src/gpu/page/background.rs+54-17| ... | ... | @@ -402,6 +402,10 @@ const MARKER: &str = "Snowbound template"; |
| 402 | 402 | /// device pixels per point. |
| 403 | 403 | const MAX_RASTER_BYTES: f32 = 16.0 * 1024.0 * 1024.0; |
| 404 | 404 | |
| 405 | /// Art an edit brings draws in its first frame in at most this many pixels, a few | |
| 406 | /// milliseconds' work, then sharpens. | |
| 407 | const INLINE_PIXELS: f32 = 1024.0 * 1024.0; | |
| 408 | ||
| 405 | 409 | /// Template art standing in for a page's background picture. |
| 406 | 410 | pub(super) struct Background { |
| 407 | 411 | svg: &'static str, |
| ... | ... | @@ -409,14 +413,19 @@ pub(super) struct Background { |
| 409 | 413 | pub size: [f32; 2], |
| 410 | 414 | /// For light and for dark paper. |
| 411 | 415 | variants: [Variant; 2], |
| 416 | /// Whether the first raster is made on the frame's thread rather than awaited. | |
| 417 | pub inline: bool, | |
| 412 | 418 | } |
| 413 | 419 | |
| 420 | /// A raster's device pixels per point, and the dark paper it was recoloured for. | |
| 421 | type Made = (f32, Option<[f32; 4]>); | |
| 422 | ||
| 414 | 423 | #[derive(Default)] |
| 415 | 424 | struct Variant { |
| 416 | /// The raster painted and its device pixels per point. | |
| 417 | shown: Option<(RasterImage, f32)>, | |
| 418 | /// The one raster being made and its device pixels per point; replacing it abandons it. | |
| 419 | pending: Option<(Slot<RasterImage>, f32)>, | |
| 425 | /// The raster painted and what it was made for. | |
| 426 | shown: Option<(RasterImage, Made)>, | |
| 427 | /// The one raster being made and what for; replacing it abandons it. | |
| 428 | pending: Option<(Slot<RasterImage>, Made)>, | |
| 420 | 429 | } |
| 421 | 430 | |
| 422 | 431 | impl Background { |
| ... | ... | @@ -451,6 +460,7 @@ impl Background { |
| 451 | 460 | svg, |
| 452 | 461 | size, |
| 453 | 462 | variants: Default::default(), |
| 463 | inline: false, | |
| 454 | 464 | }) |
| 455 | 465 | } |
| 456 | 466 | |
| ... | ... | @@ -469,38 +479,44 @@ impl Background { |
| 469 | 479 | let density = density(scale) |
| 470 | 480 | .min((MAX_RASTER_BYTES / (4.0 * width * height)).sqrt()) |
| 471 | 481 | .min(4096.0 / width.max(height)); |
| 482 | let made = (density, dark.then_some(paper.color)); | |
| 483 | let svg = self.svg; | |
| 484 | let paper = dark.then_some(paper); | |
| 485 | let raster = move |density: f32| { | |
| 486 | let size = [width, height].map(|side| (side * density).round().max(1.0) as u32); | |
| 487 | move || rasterize(svg, paper, size) | |
| 488 | }; | |
| 472 | 489 | let variant = &mut self.variants[usize::from(dark)]; |
| 473 | 490 | let landed = variant |
| 474 | 491 | .pending |
| 475 | 492 | .as_ref() |
| 476 | 493 | .and_then(|(slot, made)| Some((slot.lock().unwrap().take()?, *made))); |
| 477 | if let Some((image, made)) = landed { | |
| 478 | if made == density || variant.shown.is_none() { | |
| 479 | variant.shown = Some((image, made)); | |
| 494 | if let Some((image, landed)) = landed { | |
| 495 | if landed == made || variant.shown.is_none() { | |
| 496 | variant.shown = Some((image, landed)); | |
| 480 | 497 | } |
| 481 | 498 | variant.pending = None; |
| 482 | 499 | } |
| 500 | if variant.shown.is_none() && std::mem::take(&mut self.inline) { | |
| 501 | let quick = density.min((INLINE_PIXELS / (width * height)).sqrt()); | |
| 502 | variant.shown = Some((raster(quick)(), (quick, made.1))); | |
| 503 | } | |
| 483 | 504 | let shown = variant.shown.as_ref().map(|(_, made)| *made); |
| 484 | if shown != Some(density) | |
| 485 | && variant.pending.as_ref().map(|(_, made)| *made) != Some(density) | |
| 486 | { | |
| 487 | let svg = self.svg; | |
| 488 | let paper = dark.then_some(paper); | |
| 489 | let size = [width, height].map(|side| (side * density).round().max(1.0) as u32); | |
| 505 | if shown != Some(made) && variant.pending.as_ref().map(|(_, made)| *made) != Some(made) { | |
| 490 | 506 | let start = Instant::now() |
| 491 | 507 | + if shown.is_some() { |
| 492 | 508 | SETTLE |
| 493 | 509 | } else { |
| 494 | 510 | Duration::ZERO |
| 495 | 511 | }; |
| 496 | let slot = queue(start, waker, move || rasterize(svg, paper, size)); | |
| 497 | variant.pending = Some((slot, density)); | |
| 512 | let slot = queue(start, waker, raster(density)); | |
| 513 | variant.pending = Some((slot, made)); | |
| 498 | 514 | } |
| 499 | shown == Some(density) | |
| 515 | shown == Some(made) | |
| 500 | 516 | } |
| 501 | 517 | } |
| 502 | 518 | |
| 503 | fn dark(paper: Paper) -> bool { | |
| 519 | pub(super) fn dark(paper: Paper) -> bool { | |
| 504 | 520 | let [lightness, ..] = draw::oklab(paper.color); |
| 505 | 521 | let [ink, ..] = draw::oklab(paper.ink); |
| 506 | 522 | ink > lightness |
| ... | ... | @@ -514,6 +530,19 @@ fn rasterize(svg: &str, dark: Option<Paper>, size: [u32; 2]) -> RasterImage { |
| 514 | 530 | RasterImage::new(size, pixels).expect("Art is within the image budget") |
| 515 | 531 | } |
| 516 | 532 | |
| 533 | /// `svg` as a PNG `side` pixels square. | |
| 534 | pub(super) fn png(svg: &str, side: u32) -> Vec<u8> { | |
| 535 | let mut encoded = Vec::new(); | |
| 536 | let mut encoder = png::Encoder::new(&mut encoded, side, side); | |
| 537 | encoder.set_color(png::ColorType::Rgba); | |
| 538 | encoder.set_depth(png::BitDepth::Eight); | |
| 539 | encoder | |
| 540 | .write_header() | |
| 541 | .and_then(|mut writer| writer.write_image_data(&render(svg, [side; 2]))) | |
| 542 | .expect("A PNG encodes into memory"); | |
| 543 | encoded | |
| 544 | } | |
| 545 | ||
| 517 | 546 | /// Straight-alpha RGBA rows of `svg` stretched to `width` by `height` pixels. |
| 518 | 547 | fn render(svg: &str, [width, height]: [u32; 2]) -> Vec<u8> { |
| 519 | 548 | let tree = parse(svg); |
| ... | ... | @@ -747,6 +776,7 @@ mod tests { |
| 747 | 776 | svg: TEMPLATES.iter().find(|t| t.name == "ivy").unwrap().svg, |
| 748 | 777 | size: [64.0, 48.0], |
| 749 | 778 | variants: Default::default(), |
| 779 | inline: false, | |
| 750 | 780 | } |
| 751 | 781 | } |
| 752 | 782 | |
| ... | ... | @@ -805,6 +835,13 @@ mod tests { |
| 805 | 835 | } |
| 806 | 836 | } |
| 807 | 837 | |
| 838 | #[test] | |
| 839 | fn every_template_s_art_is_recreated() { | |
| 840 | for art in crate::template::TEMPLATES.iter().flat_map(|t| t.art) { | |
| 841 | assert!(TEMPLATES.iter().any(|t| t.name == art.art), "{}", art.art); | |
| 842 | } | |
| 843 | } | |
| 844 | ||
| 808 | 845 | #[test] |
| 809 | 846 | fn our_template_pictures_are_recognised_as_the_template() { |
| 810 | 847 | for (name, magic, size) in [ |
crates/canvas/src/interaction/accessibility.rs+8-31| ... | ... | @@ -594,38 +594,15 @@ fn runs_of(shaped: &ParagraphLayout, breaks: bool) -> Result<Vec<(Node, Vec<u32> |
| 594 | 594 | .tags |
| 595 | 595 | .iter() |
| 596 | 596 | .map(|tag| { |
| 597 | use crate::outline::TagIcon; | |
| 598 | let fallback = match tag.icon { | |
| 599 | TagIcon::CheckBox { .. } => "To do", | |
| 600 | TagIcon::Star => "Important", | |
| 601 | TagIcon::Question => "Question", | |
| 602 | TagIcon::Highlight => "Highlight", | |
| 603 | TagIcon::Contact => "Contact", | |
| 604 | TagIcon::Address => "Address", | |
| 605 | TagIcon::Phone => "Phone number", | |
| 606 | TagIcon::Music => "Music", | |
| 607 | TagIcon::Exclamation => "Critical", | |
| 608 | TagIcon::RedSquare => "Project A", | |
| 609 | TagIcon::YellowSquare => "Project B", | |
| 610 | TagIcon::BlueSquare => "Project C", | |
| 611 | TagIcon::Flag => "Outlook task", | |
| 612 | TagIcon::Idea => "Idea", | |
| 613 | TagIcon::Password => "Password", | |
| 614 | TagIcon::Movie => "Movie to see", | |
| 615 | TagIcon::Book => "Book to read", | |
| 616 | TagIcon::Web => "Web site to visit", | |
| 617 | TagIcon::Blog => "Remember for blog", | |
| 618 | TagIcon::Email => "Send in email", | |
| 619 | TagIcon::Other => "Tag", | |
| 620 | }; | |
| 621 | let label = if tag.label.is_empty() { | |
| 622 | fallback | |
| 623 | } else { | |
| 624 | &tag.label | |
| 597 | use crate::outline::{TagIcon, symbol_name}; | |
| 598 | let label = match tag.icon { | |
| 599 | _ if !tag.label.is_empty() => &tag.label, | |
| 600 | TagIcon::Symbol { shape, .. } => symbol_name(shape).unwrap_or("Tag"), | |
| 601 | TagIcon::Task => "Outlook task", | |
| 625 | 602 | }; |
| 626 | 603 | let state = match tag.icon { |
| 627 | TagIcon::CheckBox { checked: true, .. } => ", completed", | |
| 628 | TagIcon::CheckBox { checked: false, .. } => ", incomplete", | |
| 604 | TagIcon::Symbol { checked: true, .. } => ", completed", | |
| 605 | icon if icon.checkable() => ", incomplete", | |
| 629 | 606 | _ => "", |
| 630 | 607 | }; |
| 631 | 608 | format!( |
| ... | ... | @@ -1026,7 +1003,7 @@ pub(super) mod tests { |
| 1026 | 1003 | "Rehearsal, incomplete", |
| 1027 | 1004 | "Rehearsal, completed", |
| 1028 | 1005 | "Question", |
| 1029 | "Music, disabled" | |
| 1006 | "Music Note, disabled" | |
| 1030 | 1007 | ] |
| 1031 | 1008 | ); |
| 1032 | 1009 | let text = state.focus().unwrap().document_range().text(); |
crates/canvas/src/interaction/mod.rs+146-20| ... | ... | @@ -60,6 +60,8 @@ pub enum Request { |
| 60 | 60 | Paste, |
| 61 | 61 | /// Open a link's address, as a click on a link does. |
| 62 | 62 | OpenLink(String), |
| 63 | /// Open a copy of a file with the system's application for it, as a double click does. | |
| 64 | OpenAttachment(onestore::page::Attachment), | |
| 63 | 65 | } |
| 64 | 66 | |
| 65 | 67 | /// What a secondary press lands on, which its context menu offers commands for. |
| ... | ... | @@ -72,6 +74,8 @@ pub struct Context { |
| 72 | 74 | pub selected: bool, |
| 73 | 75 | /// The paragraph at the caret, which Copy Link to Paragraph names. |
| 74 | 76 | pub paragraph: Option<onestore::ExGuid>, |
| 77 | /// The file the press selected, which Open and Save As take. | |
| 78 | pub attachment: Option<onestore::page::Attachment>, | |
| 75 | 79 | } |
| 76 | 80 | |
| 77 | 81 | /// What an event did: `changed` means the page or selection changed (the host saves, |
| ... | ... | @@ -175,13 +179,15 @@ enum PointerFeedback<'a> { |
| 175 | 179 | pub enum ObjectFocus { |
| 176 | 180 | ReadOnly(usize), |
| 177 | 181 | Image(onestore::ExGuid), |
| 182 | /// A file in an outline's flow or on the page. | |
| 183 | File(onestore::ExGuid), | |
| 178 | 184 | } |
| 179 | 185 | |
| 180 | 186 | impl ObjectFocus { |
| 181 | 187 | pub fn read_only(self) -> Option<usize> { |
| 182 | 188 | match self { |
| 183 | 189 | Self::ReadOnly(index) => Some(index), |
| 184 | Self::Image(_) => None, | |
| 190 | Self::Image(_) | Self::File(_) => None, | |
| 185 | 191 | } |
| 186 | 192 | } |
| 187 | 193 | } |
| ... | ... | @@ -519,6 +525,7 @@ impl PageView { |
| 519 | 525 | let (origin, size) = self.editor.image_placement(id).unwrap(); |
| 520 | 526 | image_rect(origin, size) |
| 521 | 527 | } |
| 528 | ObjectFocus::File(id) => self.editor.attachment_rect(id).unwrap(), | |
| 522 | 529 | }; |
| 523 | 530 | [ |
| 524 | 531 | x0 + offset[0], |
| ... | ... | @@ -861,7 +868,7 @@ impl PageView { |
| 861 | 868 | { |
| 862 | 869 | Cursor::Pointer |
| 863 | 870 | } |
| 864 | (None, Some(Hit::ReadOnly(_) | Hit::Check { .. })) => Cursor::Default, | |
| 871 | (None, Some(Hit::ReadOnly(_) | Hit::Check { .. } | Hit::File(_))) => Cursor::Default, | |
| 865 | 872 | _ => Cursor::Text, |
| 866 | 873 | } |
| 867 | 874 | } |
| ... | ... | @@ -998,6 +1005,24 @@ impl PageView { |
| 998 | 1005 | .click_check(&mut self.engine, outline, paragraph)?; |
| 999 | 1006 | return self.changed(); |
| 1000 | 1007 | } |
| 1008 | Some(Hit::File(id)) => { | |
| 1009 | self.set_object_focus(Some(ObjectFocus::File(id))); | |
| 1010 | self.drag = None; | |
| 1011 | if unit != SelectionUnit::Grapheme | |
| 1012 | && let Some(file) = self.editor.attachment(id) | |
| 1013 | { | |
| 1014 | return Ok(Response::request(Request::OpenAttachment(file.clone()))); | |
| 1015 | } | |
| 1016 | // A file on the page drags as a picture does. | |
| 1017 | if !self.editor.image_in_outline(id) { | |
| 1018 | self.drag = Some(Drag::Image { | |
| 1019 | id, | |
| 1020 | handle: [0, 0], | |
| 1021 | press: point, | |
| 1022 | pending_press: Some(self.pointer), | |
| 1023 | }); | |
| 1024 | } | |
| 1025 | } | |
| 1001 | 1026 | Some(Hit::Image { id, handle }) => { |
| 1002 | 1027 | self.set_object_focus(Some(ObjectFocus::Image(id))); |
| 1003 | 1028 | self.drag = Some(Drag::Image { |
| ... | ... | @@ -1062,15 +1087,7 @@ impl PageView { |
| 1062 | 1087 | }); |
| 1063 | 1088 | } |
| 1064 | 1089 | None => { |
| 1065 | // A fresh click places text 7 px above the pointer, on the grid. | |
| 1066 | let position = [point[0], point[1] - 7.0 * self.pixel()]; | |
| 1067 | let position = if self.modifiers.option { | |
| 1068 | position | |
| 1069 | } else { | |
| 1070 | snap_to_grid(position, self.editor.margin_origin()) | |
| 1071 | }; | |
| 1072 | self.editor | |
| 1073 | .place_caret(&mut self.engine, position, DEFAULT_OUTLINE_WIDTH)?; | |
| 1090 | self.place_caret(point)?; | |
| 1074 | 1091 | self.set_object_focus(None); |
| 1075 | 1092 | self.drag = Some(Drag::Text { |
| 1076 | 1093 | anchor: self.editor.selection(), |
| ... | ... | @@ -1081,6 +1098,19 @@ impl PageView { |
| 1081 | 1098 | self.changed() |
| 1082 | 1099 | } |
| 1083 | 1100 | |
| 1101 | /// A fresh click places text 7 px above the pointer, on the grid. | |
| 1102 | fn place_caret(&mut self, point: [f32; 2]) -> Result<()> { | |
| 1103 | let position = [point[0], point[1] - 7.0 * self.pixel()]; | |
| 1104 | let position = if self.modifiers.option { | |
| 1105 | position | |
| 1106 | } else { | |
| 1107 | snap_to_grid(position, self.editor.margin_origin()) | |
| 1108 | }; | |
| 1109 | self.editor | |
| 1110 | .place_caret(&mut self.engine, position, DEFAULT_OUTLINE_WIDTH)?; | |
| 1111 | Ok(()) | |
| 1112 | } | |
| 1113 | ||
| 1084 | 1114 | pub fn pointer_released(&mut self) -> Result<Response> { |
| 1085 | 1115 | if let Some(Drag::Space { axis, line }) = self.drag { |
| 1086 | 1116 | self.drag = None; |
| ... | ... | @@ -1261,11 +1291,22 @@ impl PageView { |
| 1261 | 1291 | } |
| 1262 | 1292 | |
| 1263 | 1293 | /// A secondary press at the pointer: text there takes the caret unless it lies in the |
| 1264 | /// selection, as OneNote's context menu acts where it opens. `None` off text. | |
| 1294 | /// selection, and a file is selected, as OneNote's context menu acts where it opens. | |
| 1295 | /// `None` off text and files. | |
| 1265 | 1296 | pub fn context(&mut self) -> Result<Option<(Response, Context)>> { |
| 1266 | 1297 | let point = self.viewport.document_point(self.pointer); |
| 1267 | let Some(Hit::Text { id, point }) = self.hit_test(point) else { | |
| 1268 | return Ok(None); | |
| 1298 | let (id, point) = match self.hit_test(point) { | |
| 1299 | Some(Hit::Text { id, point }) => (id, point), | |
| 1300 | Some(Hit::File(id)) => { | |
| 1301 | let attachment = self.editor.attachment(id).cloned(); | |
| 1302 | self.set_object_focus(Some(ObjectFocus::File(id))); | |
| 1303 | let context = Context { | |
| 1304 | attachment, | |
| 1305 | ..Context::default() | |
| 1306 | }; | |
| 1307 | return Ok(Some((self.changed()?, context))); | |
| 1308 | } | |
| 1309 | _ => return Ok(None), | |
| 1269 | 1310 | }; |
| 1270 | 1311 | if self.object_focus.is_some() || !self.accepts_text() { |
| 1271 | 1312 | return Ok(None); |
| ... | ... | @@ -1296,6 +1337,7 @@ impl PageView { |
| 1296 | 1337 | .document() |
| 1297 | 1338 | .leaf(anchor.min(focus).paragraph) |
| 1298 | 1339 | .map(|(_, _, node)| node.id), |
| 1340 | attachment: None, | |
| 1299 | 1341 | }; |
| 1300 | 1342 | Ok(Some((self.changed()?, context))) |
| 1301 | 1343 | } |
| ... | ... | @@ -1363,6 +1405,44 @@ impl PageView { |
| 1363 | 1405 | self.edited() |
| 1364 | 1406 | } |
| 1365 | 1407 | |
| 1408 | /// Insert, Attach File: see [`CanvasEditor::insert_attachment`]. A file without an icon | |
| 1409 | /// takes the page's blank one. | |
| 1410 | pub fn insert_attachment(&mut self, mut file: onestore::page::Attachment) -> Result<Response> { | |
| 1411 | if !self.accepts_text() || self.editor.marked_range().is_some() { | |
| 1412 | return Ok(Response::default()); | |
| 1413 | } | |
| 1414 | file.preview | |
| 1415 | .get_or_insert_with(|| crate::gpu::page::file_icon().into()); | |
| 1416 | self.editor.insert_attachment(&mut self.engine, file)?; | |
| 1417 | self.follow_pictures()?; | |
| 1418 | self.edited() | |
| 1419 | } | |
| 1420 | ||
| 1421 | /// A file dropped at view point `position`, in device pixels: text or blank page there | |
| 1422 | /// takes the caret as a click would, then the file is attached at the caret, on the page | |
| 1423 | /// where the caret is on blank page. | |
| 1424 | pub fn drop_attachment( | |
| 1425 | &mut self, | |
| 1426 | position: [f32; 2], | |
| 1427 | file: onestore::page::Attachment, | |
| 1428 | ) -> Result<Response> { | |
| 1429 | let point = self.viewport.document_point(position); | |
| 1430 | self.set_object_focus(None); | |
| 1431 | match self.hit_test(point) { | |
| 1432 | Some(Hit::Text { id, point }) => { | |
| 1433 | self.editor.focus_outline(id)?; | |
| 1434 | self.editor.select_below(&mut self.engine, id, point)?; | |
| 1435 | let at = self | |
| 1436 | .editor | |
| 1437 | .selection_at(point[0], point[1], SelectionUnit::Grapheme)?; | |
| 1438 | self.editor.select(at)?; | |
| 1439 | } | |
| 1440 | None => self.place_caret(point)?, | |
| 1441 | _ => {} | |
| 1442 | } | |
| 1443 | self.insert_attachment(file) | |
| 1444 | } | |
| 1445 | ||
| 1366 | 1446 | /// Professional, or Linear with `linear`, on the equation at the caret. |
| 1367 | 1447 | pub fn switch_equation(&mut self, linear: bool) -> Result<Response> { |
| 1368 | 1448 | if linear { |
| ... | ... | @@ -1380,7 +1460,8 @@ impl PageView { |
| 1380 | 1460 | } else { |
| 1381 | 1461 | self.editor.undo(&mut self.engine)?; |
| 1382 | 1462 | } |
| 1383 | if let Some(ObjectFocus::Image(id)) = self.object_focus | |
| 1463 | self.follow_pictures()?; | |
| 1464 | if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus | |
| 1384 | 1465 | && self.editor.image_placement(id).is_none() |
| 1385 | 1466 | { |
| 1386 | 1467 | self.set_object_focus(None); |
| ... | ... | @@ -1404,18 +1485,29 @@ impl PageView { |
| 1404 | 1485 | self.changed() |
| 1405 | 1486 | } |
| 1406 | 1487 | |
| 1407 | /// Gives the page a colour (COLORREF) and rule lines, as one undo step. | |
| 1488 | /// Gives the page a colour (COLORREF), rule lines and, when given, `art` in place of its | |
| 1489 | /// background pictures, as one undo step: see [`CanvasEditor::set_paper`]. | |
| 1408 | 1490 | pub fn set_paper( |
| 1409 | 1491 | &mut self, |
| 1410 | 1492 | color: Option<u32>, |
| 1411 | 1493 | rule_lines: Option<onestore::page::RuleLines>, |
| 1494 | art: Option<Vec<onestore::page::Image>>, | |
| 1412 | 1495 | ) -> Result<Response> { |
| 1413 | if !self.editor.set_paper(color, rule_lines) { | |
| 1496 | if !self.editor.set_paper(color, rule_lines, art) { | |
| 1414 | 1497 | return Ok(Response::default()); |
| 1415 | 1498 | } |
| 1499 | self.follow_pictures()?; | |
| 1416 | 1500 | self.changed() |
| 1417 | 1501 | } |
| 1418 | 1502 | |
| 1503 | /// Draws page-level pictures an edit or undo brought to the page. | |
| 1504 | fn follow_pictures(&mut self) -> Result<()> { | |
| 1505 | if let Some((scene, _)) = &mut self.scene { | |
| 1506 | scene.follow(&mut self.editor, &mut self.engine)?; | |
| 1507 | } | |
| 1508 | Ok(()) | |
| 1509 | } | |
| 1510 | ||
| 1419 | 1511 | /// Gives keyboard focus to a read-only object, as an assistive technology asks. |
| 1420 | 1512 | pub fn focus_read_only(&mut self, index: usize) -> Result<Response> { |
| 1421 | 1513 | self.set_object_focus(Some(ObjectFocus::ReadOnly(index))); |
| ... | ... | @@ -1439,14 +1531,14 @@ impl PageView { |
| 1439 | 1531 | if key == &Key::Named(NamedKey::Modifier) { |
| 1440 | 1532 | return Ok(Response::default()); |
| 1441 | 1533 | } |
| 1442 | if let Some(ObjectFocus::Image(id)) = self.object_focus | |
| 1534 | if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus | |
| 1443 | 1535 | && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete)) |
| 1444 | 1536 | { |
| 1445 | 1537 | self.editor.remove_image(&mut self.engine, id)?; |
| 1446 | 1538 | self.set_object_focus(None); |
| 1447 | 1539 | return self.changed(); |
| 1448 | 1540 | } |
| 1449 | if let Some(ObjectFocus::Image(id)) = self.object_focus | |
| 1541 | if let Some(ObjectFocus::Image(id) | ObjectFocus::File(id)) = self.object_focus | |
| 1450 | 1542 | && !(shift || command || option || control) |
| 1451 | 1543 | && let Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) = key |
| 1452 | 1544 | && self.editor.step_from_image( |
| ... | ... | @@ -1671,6 +1763,8 @@ pub enum Hit { |
| 1671 | 1763 | id: onestore::ExGuid, |
| 1672 | 1764 | handle: [i8; 2], |
| 1673 | 1765 | }, |
| 1766 | /// A file in an outline's flow or on the page, its icon or name. | |
| 1767 | File(onestore::ExGuid), | |
| 1674 | 1768 | } |
| 1675 | 1769 | |
| 1676 | 1770 | /// What a document point lands on; `pixel` is document points per device pixel. |
| ... | ... | @@ -1762,7 +1856,7 @@ fn page_hit( |
| 1762 | 1856 | paragraph.tags.iter().any(|tag| { |
| 1763 | 1857 | let tag_x = left + shaped.tag_column_offset() + tag.origin[0]; |
| 1764 | 1858 | let tag_y = top + paragraph.origin[1] + tag.origin[1]; |
| 1765 | matches!(tag.icon, crate::outline::TagIcon::CheckBox { .. }) | |
| 1859 | tag.icon.checkable() | |
| 1766 | 1860 | && (tag_x..=tag_x + tag.size).contains(&x) |
| 1767 | 1861 | && (tag_y..=tag_y + tag.size).contains(&y) |
| 1768 | 1862 | }) |
| ... | ... | @@ -1791,6 +1885,14 @@ fn page_hit( |
| 1791 | 1885 | handle: [0, 0], |
| 1792 | 1886 | }); |
| 1793 | 1887 | } |
| 1888 | if let Some(file) = outline.shaped().objects.iter().find(|object| { | |
| 1889 | let [x0, y0, x1, y1] = object.bounds(); | |
| 1890 | matches!(object.kind, crate::outline::ObjectKind::File(_)) | |
| 1891 | && (x0..=x1).contains(&inner[0]) | |
| 1892 | && (y0..=y1).contains(&inner[1]) | |
| 1893 | }) { | |
| 1894 | return Some(Hit::File(file.id)); | |
| 1895 | } | |
| 1794 | 1896 | if x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3] { |
| 1795 | 1897 | return Some(Hit::Text { |
| 1796 | 1898 | id: outline.id, |
| ... | ... | @@ -1826,6 +1928,7 @@ fn page_hit( |
| 1826 | 1928 | crate::gpu::page::SceneHit::Date(field) => Some(Hit::Date(field)), |
| 1827 | 1929 | crate::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), |
| 1828 | 1930 | crate::gpu::page::SceneHit::Image(id) => Some(Hit::Image { id, handle: [0, 0] }), |
| 1931 | crate::gpu::page::SceneHit::File(id) => Some(Hit::File(id)), | |
| 1829 | 1932 | } |
| 1830 | 1933 | }; |
| 1831 | 1934 | [Layer::Grips, Layer::Body, Layer::Below] |
| ... | ... | @@ -2010,6 +2113,29 @@ fn page_primitives<'a>( |
| 2010 | 2113 | }; |
| 2011 | 2114 | append_image_chrome(image_rect(origin, size), paint.pixel, &mut primitives); |
| 2012 | 2115 | } |
| 2116 | // OneNote shades a selected file's column and frames it with dashes. | |
| 2117 | if let Some(ObjectFocus::File(id)) = object_focus { | |
| 2118 | let rect = match preview { | |
| 2119 | Some(PointerFeedback::Image(moving, origin, size)) if moving == id => { | |
| 2120 | image_rect(origin, size) | |
| 2121 | } | |
| 2122 | _ => editor | |
| 2123 | .attachment_rect(id) | |
| 2124 | .ok_or("The selected file is missing.")?, | |
| 2125 | }; | |
| 2126 | primitives.extend([ | |
| 2127 | Primitive::Rect { | |
| 2128 | rect, | |
| 2129 | color: [0.0, 0.0, 0.0, 0.08], | |
| 2130 | }, | |
| 2131 | Primitive::RoundedRect { | |
| 2132 | rect, | |
| 2133 | radius: [0.0; 2], | |
| 2134 | stroke: Some(Stroke::Dashed(paint.pixel)), | |
| 2135 | color: [0.25, 0.45, 0.7, 1.0], | |
| 2136 | }, | |
| 2137 | ]); | |
| 2138 | } | |
| 2013 | 2139 | if let Some(ObjectFocus::ReadOnly(index)) = object_focus { |
| 2014 | 2140 | let (scene, offset) = scene.unwrap(); |
| 2015 | 2141 | let [x0, y0, x1, y1] = scene.read_only(Some(editor)).nth(index).unwrap().rect(); |
crates/canvas/src/interaction/tests.rs+264-7| ... | ... | @@ -1805,7 +1805,7 @@ fn outline_chrome_grows_left_to_contain_its_tag_column() { |
| 1805 | 1805 | assert!(frame[1] < y && y + size < frame[3], "{frame:?} {y}"); |
| 1806 | 1806 | // OneNote 2010 shows an arrow over a check box, which a click toggles. |
| 1807 | 1807 | let hit = page_hit_test(editor, None, [x + 1.0, y + 1.0], pixel); |
| 1808 | if matches!(tag.icon, crate::outline::TagIcon::CheckBox { .. }) { | |
| 1808 | if tag.icon.checkable() { | |
| 1809 | 1809 | assert_eq!( |
| 1810 | 1810 | hit, |
| 1811 | 1811 | Some(Hit::Check { |
| ... | ... | @@ -1821,18 +1821,30 @@ fn outline_chrome_grows_left_to_contain_its_tag_column() { |
| 1821 | 1821 | }; |
| 1822 | 1822 | check(&editor, 0.0); |
| 1823 | 1823 | editor |
| 1824 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1824 | .format( | |
| 1825 | &mut engine, | |
| 1826 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1827 | ) | |
| 1825 | 1828 | .unwrap(); |
| 1826 | 1829 | check(&editor, 1.0); |
| 1827 | 1830 | editor |
| 1828 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) | |
| 1831 | .format( | |
| 1832 | &mut engine, | |
| 1833 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), | |
| 1834 | ) | |
| 1829 | 1835 | .unwrap(); |
| 1830 | 1836 | check(&editor, 2.0); |
| 1831 | 1837 | editor |
| 1832 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1838 | .format( | |
| 1839 | &mut engine, | |
| 1840 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1841 | ) | |
| 1833 | 1842 | .unwrap(); |
| 1834 | 1843 | editor |
| 1835 | .format(&mut engine, Formatting::Tag(NoteTag::Question)) | |
| 1844 | .format( | |
| 1845 | &mut engine, | |
| 1846 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), | |
| 1847 | ) | |
| 1836 | 1848 | .unwrap(); |
| 1837 | 1849 | check(&editor, 0.0); |
| 1838 | 1850 | } |
| ... | ... | @@ -1851,7 +1863,10 @@ fn a_click_on_a_check_box_toggles_it_under_an_arrow() { |
| 1851 | 1863 | let mut editor = |
| 1852 | 1864 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); |
| 1853 | 1865 | editor |
| 1854 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1866 | .format( | |
| 1867 | &mut engine, | |
| 1868 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1869 | ) | |
| 1855 | 1870 | .unwrap(); |
| 1856 | 1871 | let outline = editor.active_outline(); |
| 1857 | 1872 | let (_, paragraph) = outline.layouts().next().unwrap(); |
| ... | ... | @@ -1923,7 +1938,10 @@ fn a_page_opens_where_onenote_places_the_view() { |
| 1923 | 1938 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); |
| 1924 | 1939 | if tag { |
| 1925 | 1940 | editor |
| 1926 | .format(&mut engine, Formatting::Tag(NoteTag::ToDo)) | |
| 1941 | .format( | |
| 1942 | &mut engine, | |
| 1943 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), | |
| 1944 | ) | |
| 1927 | 1945 | .unwrap(); |
| 1928 | 1946 | } |
| 1929 | 1947 | let reached = reach(editor.active_outline()); |
| ... | ... | @@ -2625,3 +2643,242 @@ fn rule_lines_start_at_the_margin_origin() { |
| 2625 | 2643 | [0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0, 96.0] |
| 2626 | 2644 | ); |
| 2627 | 2645 | } |
| 2646 | ||
| 2647 | /// Template art the Page Color menu gives draws in the frame after, over the rule lines as | |
| 2648 | /// OneNote's opaque pictures hide them, and follows undo and redo without a reload. | |
| 2649 | #[test] | |
| 2650 | fn art_draws_at_once_over_the_rule_lines_and_follows_undo() { | |
| 2651 | let mut engine = TextEngine::default(); | |
| 2652 | let outline = TextOutline::new( | |
| 2653 | &mut engine, | |
| 2654 | TextDocument::new(vec![Paragraph::new("Notes".into(), Format::default())]).unwrap(), | |
| 2655 | 300.0, | |
| 2656 | [300.0, 100.0], | |
| 2657 | ) | |
| 2658 | .unwrap() | |
| 2659 | .snapshot(); | |
| 2660 | let page = Page { | |
| 2661 | identity: None, | |
| 2662 | title: String::new(), | |
| 2663 | created: None, | |
| 2664 | margin_origin: [36.0, 14.4], | |
| 2665 | color: None, | |
| 2666 | rule_lines: Some(crate::template::RULE_LINES[2].1), | |
| 2667 | definitions: Default::default(), | |
| 2668 | objects: vec![onestore::page::PageObject::Outline(outline)], | |
| 2669 | }; | |
| 2670 | let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap(); | |
| 2671 | let mut view = PageView::new( | |
| 2672 | editor, | |
| 2673 | engine, | |
| 2674 | Some((scene, [0.0; 2])), | |
| 2675 | [800, 600], | |
| 2676 | 1.0, | |
| 2677 | Duration::from_millis(500), | |
| 2678 | ); | |
| 2679 | let ivy = crate::template::find("Ivy").unwrap(); | |
| 2680 | let art = ivy.pictures().unwrap(); | |
| 2681 | let [x, y] = ivy.art[0].position; | |
| 2682 | let [width, height] = ivy.art[0].size.unwrap(); | |
| 2683 | let drawn = |view: &mut PageView, paper: Paper| { | |
| 2684 | view.update_pictures(paper, std::task::Waker::noop()); | |
| 2685 | let primitives = view.primitives(TextColors { paper, ..COLORS }).unwrap(); | |
| 2686 | let backing = primitives.iter().position(|primitive| { | |
| 2687 | matches!(primitive, Primitive::Rect { rect, color } | |
| 2688 | if *rect == [x, y, x + width, y + height] && *color == paper.color) | |
| 2689 | }); | |
| 2690 | let image = primitives | |
| 2691 | .iter() | |
| 2692 | .position(|primitive| matches!(primitive, Primitive::Image { .. })); | |
| 2693 | let lines = primitives.iter().rposition( | |
| 2694 | |primitive| matches!(primitive, Primitive::Rect { rect, .. } if rect[0] < x), | |
| 2695 | ); | |
| 2696 | (backing, image, lines) | |
| 2697 | }; | |
| 2698 | let _ = view | |
| 2699 | .set_paper(None, view.editor.rule_lines(), Some(art)) | |
| 2700 | .unwrap(); | |
| 2701 | let (backing, image, lines) = drawn(&mut view, Paper::WHITE); | |
| 2702 | assert!( | |
| 2703 | lines < backing && backing < image, | |
| 2704 | "{lines:?} {backing:?} {image:?}" | |
| 2705 | ); | |
| 2706 | let dark = Paper { | |
| 2707 | color: [0.0137, 0.0144, 0.0159, 1.0], | |
| 2708 | ink: [0.791, 0.791, 0.791, 1.0], | |
| 2709 | }; | |
| 2710 | // Dark paper hides the lines in its own colour while its recolouring is made. | |
| 2711 | assert!(matches!(drawn(&mut view, dark), (Some(_), None, _))); | |
| 2712 | let _ = view.undo(false).unwrap(); | |
| 2713 | assert!(matches!(drawn(&mut view, Paper::WHITE), (None, None, _))); | |
| 2714 | let _ = view.undo(true).unwrap(); | |
| 2715 | assert!(matches!( | |
| 2716 | drawn(&mut view, Paper::WHITE), | |
| 2717 | (Some(_), Some(_), _) | |
| 2718 | )); | |
| 2719 | } | |
| 2720 | ||
| 2721 | #[test] | |
| 2722 | fn an_attached_file_selects_opens_on_a_double_click_and_deletes() { | |
| 2723 | let (mut view, _) = picture_view(); | |
| 2724 | let now = Instant::now(); | |
| 2725 | let file = onestore::page::Attachment { | |
| 2726 | id: onestore::page::text::new_id().unwrap(), | |
| 2727 | filename: "notes.txt".into(), | |
| 2728 | source_path: None, | |
| 2729 | size: Some([24.0, 24.0]), | |
| 2730 | layout: Default::default(), | |
| 2731 | bytes: Some(Arc::from(b"notes".as_slice())), | |
| 2732 | preview: None, | |
| 2733 | recording: None, | |
| 2734 | }; | |
| 2735 | view.editor | |
| 2736 | .move_selection(&mut view.engine, Movement::DocumentEnd, false) | |
| 2737 | .unwrap(); | |
| 2738 | assert!(view.insert_attachment(file.clone()).unwrap().changed); | |
| 2739 | let stored = view.editor.attachment(file.id).unwrap().clone(); | |
| 2740 | // OneNote draws a broken picture for a file stored without its icon. | |
| 2741 | assert!(stored.preview.as_ref().unwrap().starts_with(b"\x89PNG")); | |
| 2742 | view.primitives(COLORS).unwrap(); | |
| 2743 | ||
| 2744 | let [x0, y0, x1, y1] = view.editor.attachment_rect(file.id).unwrap(); | |
| 2745 | let center = [(x0 + x1) / 2.0, (y0 + y1) / 2.0]; | |
| 2746 | click(&mut view, center, now); | |
| 2747 | assert_eq!(view.object_focus(), Some(ObjectFocus::File(file.id))); | |
| 2748 | assert!(!view.accepts_text()); | |
| 2749 | view.primitives(COLORS).unwrap(); | |
| 2750 | let _ = view.pointer_moved(center).unwrap(); | |
| 2751 | assert_eq!( | |
| 2752 | view.pointer_pressed(now + Duration::from_millis(100)) | |
| 2753 | .unwrap() | |
| 2754 | .request, | |
| 2755 | Some(Request::OpenAttachment(stored.clone())) | |
| 2756 | ); | |
| 2757 | let _ = view.pointer_released().unwrap(); | |
| 2758 | let (_, context) = view.context().unwrap().unwrap(); | |
| 2759 | assert_eq!(context.attachment, Some(stored)); | |
| 2760 | ||
| 2761 | assert!( | |
| 2762 | view.key(&Key::Named(NamedKey::Delete), None) | |
| 2763 | .unwrap() | |
| 2764 | .changed | |
| 2765 | ); | |
| 2766 | assert!(view.editor.attachment(file.id).is_none()); | |
| 2767 | assert!(view.accepts_text()); | |
| 2768 | } | |
| 2769 | ||
| 2770 | /// OneNote 2010 puts a file attached or dropped on blank page on the page there, where it | |
| 2771 | /// selects, drags on the grid, opens and deletes as one (`corpus/attachment-floating`). | |
| 2772 | #[test] | |
| 2773 | fn a_file_dropped_on_blank_page_lies_on_the_page_and_drags() { | |
| 2774 | use onestore::op::PageOp; | |
| 2775 | let mut engine = TextEngine::default(); | |
| 2776 | let document = | |
| 2777 | TextDocument::new(vec![Paragraph::new("Text".into(), Default::default())]).unwrap(); | |
| 2778 | let outline = TextOutline::new(&mut engine, document, 240.0, [36.0, 36.0]).unwrap(); | |
| 2779 | let page = Page { | |
| 2780 | title: String::new(), | |
| 2781 | identity: None, | |
| 2782 | created: None, | |
| 2783 | margin_origin: [36.0, 14.4], | |
| 2784 | color: None, | |
| 2785 | rule_lines: None, | |
| 2786 | definitions: Default::default(), | |
| 2787 | objects: vec![onestore::page::PageObject::Outline(outline.snapshot())], | |
| 2788 | }; | |
| 2789 | let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap(); | |
| 2790 | let mut view = PageView::new( | |
| 2791 | editor, | |
| 2792 | engine, | |
| 2793 | Some((scene, [0.0; 2])), | |
| 2794 | [800, 600], | |
| 2795 | 1.0, | |
| 2796 | Duration::from_millis(500), | |
| 2797 | ); | |
| 2798 | view.viewport.scale = 1.0; | |
| 2799 | view.viewport.origin = [0.0; 2]; | |
| 2800 | let _ = view.editor.take_ops().unwrap(); | |
| 2801 | let file = onestore::page::Attachment { | |
| 2802 | id: onestore::page::text::new_id().unwrap(), | |
| 2803 | filename: "float.txt".into(), | |
| 2804 | source_path: None, | |
| 2805 | size: Some([24.0, 24.0]), | |
| 2806 | layout: Default::default(), | |
| 2807 | bytes: Some(Arc::from(b"float".as_slice())), | |
| 2808 | preview: None, | |
| 2809 | recording: None, | |
| 2810 | }; | |
| 2811 | let _ = view.drop_attachment([400.0, 400.0], file.clone()).unwrap(); | |
| 2812 | assert_eq!(view.editor.outlines().len(), 1); | |
| 2813 | let placed = |view: &PageView| { | |
| 2814 | view.editor | |
| 2815 | .page() | |
| 2816 | .unwrap() | |
| 2817 | .objects | |
| 2818 | .into_iter() | |
| 2819 | .find_map(|object| match object { | |
| 2820 | onestore::page::PageObject::Attachment(file) => Some(file), | |
| 2821 | _ => None, | |
| 2822 | }) | |
| 2823 | }; | |
| 2824 | // At the caret a click there places, on the 18 pt grid from the margin origin. | |
| 2825 | let stored = placed(&view).unwrap(); | |
| 2826 | assert_eq!( | |
| 2827 | [stored.layout.x, stored.layout.y], | |
| 2828 | [Some(396.0), Some(392.4)] | |
| 2829 | ); | |
| 2830 | assert_eq!(stored.layout.max_width, Some(54.0)); | |
| 2831 | assert!(matches!( | |
| 2832 | &view.editor.take_ops().unwrap()[..], | |
| 2833 | [PageOp::Add { object: onestore::page::PageObject::Attachment(added), before: None }] | |
| 2834 | if added.id == file.id | |
| 2835 | )); | |
| 2836 | assert!( | |
| 2837 | view.primitives(COLORS) | |
| 2838 | .unwrap() | |
| 2839 | .iter() | |
| 2840 | .any(|primitive| matches!(primitive, Primitive::Text { .. })) | |
| 2841 | ); | |
| 2842 | ||
| 2843 | let [x0, y0, x1, y1] = view.editor.attachment_rect(file.id).unwrap(); | |
| 2844 | let center = [(x0 + x1) / 2.0, (y0 + y1) / 2.0]; | |
| 2845 | let now = Instant::now(); | |
| 2846 | let _ = view.pointer_moved(center).unwrap(); | |
| 2847 | let _ = view.pointer_pressed(now).unwrap(); | |
| 2848 | assert_eq!(view.object_focus(), Some(ObjectFocus::File(file.id))); | |
| 2849 | let _ = view | |
| 2850 | .pointer_moved([center[0] + 100.0, center[1] + 50.0]) | |
| 2851 | .unwrap(); | |
| 2852 | view.primitives(COLORS).unwrap(); | |
| 2853 | let _ = view.pointer_released().unwrap(); | |
| 2854 | let moved = placed(&view).unwrap(); | |
| 2855 | assert_eq!([moved.layout.x, moved.layout.y], [Some(504.0), Some(446.4)]); | |
| 2856 | assert!(matches!( | |
| 2857 | &view.editor.take_ops().unwrap()[..], | |
| 2858 | [PageOp::Outline { object, .. }] if *object == file.id | |
| 2859 | )); | |
| 2860 | let (_, context) = view.context().unwrap().unwrap(); | |
| 2861 | assert_eq!(context.attachment.map(|found| found.id), Some(file.id)); | |
| 2862 | ||
| 2863 | let _ = view.undo(false).unwrap(); | |
| 2864 | assert_eq!(placed(&view).unwrap().layout.x, Some(396.0)); | |
| 2865 | let _ = view.undo(true).unwrap(); | |
| 2866 | assert_eq!(placed(&view).unwrap().layout.x, Some(504.0)); | |
| 2867 | let _ = view.editor.take_ops().unwrap(); | |
| 2868 | ||
| 2869 | let [x0, y0, x1, y1] = view.editor.attachment_rect(file.id).unwrap(); | |
| 2870 | click( | |
| 2871 | &mut view, | |
| 2872 | [(x0 + x1) / 2.0, (y0 + y1) / 2.0], | |
| 2873 | now + Duration::from_secs(1), | |
| 2874 | ); | |
| 2875 | let _ = view.key(&Key::Named(NamedKey::Delete), None).unwrap(); | |
| 2876 | assert!(placed(&view).is_none()); | |
| 2877 | assert!(matches!( | |
| 2878 | &view.editor.take_ops().unwrap()[..], | |
| 2879 | [PageOp::Delete { object }] if *object == file.id | |
| 2880 | )); | |
| 2881 | let _ = view.undo(false).unwrap(); | |
| 2882 | assert_eq!(placed(&view).unwrap().layout.x, Some(504.0)); | |
| 2883 | view.primitives(COLORS).unwrap(); | |
| 2884 | } |
crates/canvas/src/outline.rs+279-126| ... | ... | @@ -87,6 +87,19 @@ impl ObjectLayout { |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | 89 | |
| 90 | /// The whole object, a file's column with its name included. | |
| 91 | pub fn bounds(&self) -> [f32; 4] { | |
| 92 | match &self.kind { | |
| 93 | ObjectKind::File(label) => [ | |
| 94 | label.origin[0], | |
| 95 | self.rect[1] - 6.0, | |
| 96 | label.origin[0] + ATTACHMENT_WIDTH, | |
| 97 | self.bottom, | |
| 98 | ], | |
| 99 | _ => [self.rect[0], self.rect[1], self.rect[2], self.bottom], | |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 90 | 103 | /// The rect's top and bottom, the object's bottom and its label's top once the object |
| 91 | 104 | /// starts at `y`, given its own height, and the height it takes in the flow. |
| 92 | 105 | fn at(&self, y: f32, height: f32) -> ([f32; 4], f32) { |
| ... | ... | @@ -209,93 +222,192 @@ pub struct ParagraphLayout { |
| 209 | 222 | pub band: Option<u32>, |
| 210 | 223 | } |
| 211 | 224 | |
| 225 | /// How the page draws a note tag. | |
| 212 | 226 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] |
| 213 | 227 | pub enum TagIcon { |
| 214 | CheckBox { | |
| 215 | checked: bool, | |
| 216 | /// The badge on the small box of OneNote's check box variants. | |
| 217 | mark: Option<BoxMark>, | |
| 218 | }, | |
| 219 | Star, | |
| 220 | Question, | |
| 221 | Highlight, | |
| 222 | Contact, | |
| 223 | Address, | |
| 224 | Phone, | |
| 225 | Music, | |
| 226 | Exclamation, | |
| 227 | RedSquare, | |
| 228 | YellowSquare, | |
| 229 | BlueSquare, | |
| 230 | /// An Outlook task flag. | |
| 231 | Flag, | |
| 232 | Idea, | |
| 233 | Password, | |
| 234 | Movie, | |
| 235 | Book, | |
| 236 | Web, | |
| 237 | Blog, | |
| 238 | Email, | |
| 239 | /// A tag symbol without artwork of its own. | |
| 240 | Other, | |
| 241 | } | |
| 242 | ||
| 243 | /// What OneNote draws on a check box variant's small box. | |
| 244 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] | |
| 245 | pub enum BoxMark { | |
| 246 | Person, | |
| 247 | Manager, | |
| 248 | Arrow, | |
| 249 | One, | |
| 250 | Two, | |
| 251 | Client, | |
| 228 | /// Symbol `shape` of MS-ONE's NoteTagShape, `checked` only where it is a check box. | |
| 229 | Symbol { shape: u16, checked: bool }, | |
| 230 | /// An Outlook task, which OneNote 2010 flags. | |
| 231 | Task, | |
| 252 | 232 | } |
| 253 | 233 | |
| 254 | 234 | impl TagIcon { |
| 255 | 235 | /// How the page draws tag symbol `shape`, `checked` where it is a check box; none for |
| 256 | 236 | /// shape 0, which marks the text instead. |
| 257 | 237 | pub fn of(shape: u16, checked: bool) -> Option<Self> { |
| 258 | Some(match shape { | |
| 259 | 0 => return None, | |
| 260 | // OneNote's check boxes, plain and with a person, arrow, number or bulb. | |
| 261 | 3 => Self::CheckBox { | |
| 262 | checked, | |
| 263 | mark: None, | |
| 264 | }, | |
| 265 | 8 | 12 | 28 | 71 | 94 | 95 => Self::CheckBox { | |
| 266 | checked, | |
| 267 | mark: Some(match shape { | |
| 268 | 8 => BoxMark::Client, | |
| 269 | 12 => BoxMark::Arrow, | |
| 270 | 28 => BoxMark::One, | |
| 271 | 71 => BoxMark::Two, | |
| 272 | 94 => BoxMark::Person, | |
| 273 | _ => BoxMark::Manager, | |
| 274 | }), | |
| 275 | }, | |
| 276 | 13 => Self::Star, | |
| 277 | 15 => Self::Question, | |
| 278 | 17 => Self::Exclamation, | |
| 279 | 18 => Self::Phone, | |
| 280 | 23 => Self::Address, | |
| 281 | 118 => Self::Contact, | |
| 282 | 136 => Self::Highlight, | |
| 283 | 100 => Self::RedSquare, | |
| 284 | 101 => Self::YellowSquare, | |
| 285 | 102 => Self::BlueSquare, | |
| 286 | 121 => Self::Music, | |
| 287 | 21 => Self::Idea, | |
| 288 | 131 => Self::Password, | |
| 289 | 122 => Self::Movie, | |
| 290 | 132 => Self::Book, | |
| 291 | 125 => Self::Web, | |
| 292 | 24 => Self::Blog, | |
| 293 | 106 => Self::Email, | |
| 294 | _ => Self::Other, | |
| 238 | (shape != 0).then_some(Self::Symbol { | |
| 239 | shape, | |
| 240 | checked: checked && checkable(shape), | |
| 295 | 241 | }) |
| 296 | 242 | } |
| 243 | ||
| 244 | pub fn checkable(self) -> bool { | |
| 245 | matches!(self, Self::Symbol { shape, .. } if checkable(shape)) | |
| 246 | } | |
| 297 | 247 | } |
| 298 | 248 | |
| 249 | /// Whether tag symbol `shape` is a check box, as MS-ONE's NoteTagShape marks them. | |
| 250 | pub fn checkable(shape: u16) -> bool { | |
| 251 | matches!( | |
| 252 | shape, | |
| 253 | 1..=12 | 28 | 30 | 32 | 48 | 50 | 52 | 69 | 71 | 73 | 89..=99 | |
| 254 | ) | |
| 255 | } | |
| 256 | ||
| 257 | /// OneNote 2010's name for tag symbol `shape`, as its symbol gallery's tooltips give it; the | |
| 258 | /// follow-up flags it keeps for Outlook tasks take MS-ONE's descriptions. | |
| 259 | pub fn symbol_name(shape: u16) -> Option<&'static str> { | |
| 260 | SYMBOL_NAMES | |
| 261 | .get(usize::from(shape).checked_sub(1)?) | |
| 262 | .copied() | |
| 263 | } | |
| 264 | ||
| 265 | const SYMBOL_NAMES: [&str; 143] = [ | |
| 266 | "Green Check Box", | |
| 267 | "Yellow Check Box", | |
| 268 | "Blue Check Box", | |
| 269 | "Green Star Check Box", | |
| 270 | "Yellow Star Check Box", | |
| 271 | "Blue Star Check Box", | |
| 272 | "Green Exclamation Check Box", | |
| 273 | "Yellow Exclamation Check Box", | |
| 274 | "Blue Exclamation Check Box", | |
| 275 | "Green Arrow Check Box", | |
| 276 | "Yellow Arrow Check Box", | |
| 277 | "Blue Arrow Check Box", | |
| 278 | "Yellow Star", | |
| 279 | "Follow-up Flag", | |
| 280 | "Question", | |
| 281 | "Blue Right Arrow", | |
| 282 | "High Priority", | |
| 283 | "Telephone", | |
| 284 | "Calendar", | |
| 285 | "Clock", | |
| 286 | "Light Bulb", | |
| 287 | "Pushpin", | |
| 288 | "Home", | |
| 289 | "Comment", | |
| 290 | "Smiley", | |
| 291 | "Award Ribbon", | |
| 292 | "Key", | |
| 293 | "Blue Check Box 1", | |
| 294 | "Blue Circle 1", | |
| 295 | "Blue Check Box 2", | |
| 296 | "Blue Circle 2", | |
| 297 | "Blue Check Box 3", | |
| 298 | "Blue Circle 3", | |
| 299 | "Blue 8-Point Star", | |
| 300 | "Blue Check Mark", | |
| 301 | "Blue Circle", | |
| 302 | "Blue Down Arrow", | |
| 303 | "Blue Left Arrow", | |
| 304 | "Blue Solid Target", | |
| 305 | "Blue Star", | |
| 306 | "Blue Sun", | |
| 307 | "Blue Target", | |
| 308 | "Blue Triangle", | |
| 309 | "Blue Umbrella", | |
| 310 | "Blue Up Arrow", | |
| 311 | "Blue X with Dots", | |
| 312 | "Blue X", | |
| 313 | "Green Check Box 1", | |
| 314 | "Green Circle 1", | |
| 315 | "Green Check Box 2", | |
| 316 | "Green Circle 2", | |
| 317 | "Green Check Box 3", | |
| 318 | "Green Circle 3", | |
| 319 | "Green 8-Point Star", | |
| 320 | "Green Check Mark", | |
| 321 | "Green Circle", | |
| 322 | "Green Down Arrow", | |
| 323 | "Green Left Arrow", | |
| 324 | "Green Right Arrow", | |
| 325 | "Green Solid Target", | |
| 326 | "Green Star", | |
| 327 | "Green Sun", | |
| 328 | "Green Target", | |
| 329 | "Green Triangle", | |
| 330 | "Green Umbrella", | |
| 331 | "Green Up Arrow", | |
| 332 | "Green X with Dots", | |
| 333 | "Green X", | |
| 334 | "Yellow Check Box 1", | |
| 335 | "Yellow Circle 1", | |
| 336 | "Yellow Check Box 2", | |
| 337 | "Yellow Circle 2", | |
| 338 | "Yellow Check Box 3", | |
| 339 | "Yellow Circle 3", | |
| 340 | "Yellow 8-Point Star", | |
| 341 | "Yellow Check Mark", | |
| 342 | "Yellow Circle", | |
| 343 | "Yellow Down Arrow", | |
| 344 | "Yellow Left Arrow", | |
| 345 | "Yellow Right Arrow", | |
| 346 | "Yellow Solid Target", | |
| 347 | "Yellow Sun", | |
| 348 | "Yellow Target", | |
| 349 | "Yellow Triangle", | |
| 350 | "Yellow Umbrella", | |
| 351 | "Yellow Up Arrow", | |
| 352 | "Yellow X with Dots", | |
| 353 | "Yellow X", | |
| 354 | "Follow Up Today Flag", | |
| 355 | "Follow Up Tomorrow Flag", | |
| 356 | "Follow Up This Week Flag", | |
| 357 | "Follow Up Next Week Flag", | |
| 358 | "No Follow Up Date Flag", | |
| 359 | "To Do Blue Person", | |
| 360 | "To Do Yellow Person", | |
| 361 | "To Do Green Person", | |
| 362 | "To Do Blue Flag", | |
| 363 | "To Do Yellow Flag", | |
| 364 | "To Do Green Flag", | |
| 365 | "Red Square (Project A)", | |
| 366 | "Yellow Square (Project B)", | |
| 367 | "Blue Square (Project C)", | |
| 368 | "Green Square", | |
| 369 | "Orange Square", | |
| 370 | "Pink Square", | |
| 371 | "E-mail", | |
| 372 | "Envelope (Closed)", | |
| 373 | "Envelope (Opened)", | |
| 374 | "Mobile phone", | |
| 375 | "Telephone with Clock", | |
| 376 | "Question Balloon", | |
| 377 | "Paperclip", | |
| 378 | "Frowning", | |
| 379 | "IM contact", | |
| 380 | "Person", | |
| 381 | "Two People", | |
| 382 | "Reminder Bell", | |
| 383 | "Contact (like Outlook's)", | |
| 384 | "Flowers Bouquet", | |
| 385 | "Date", | |
| 386 | "Music Note", | |
| 387 | "Movie Clip", | |
| 388 | "Quote Mark", | |
| 389 | "Globe", | |
| 390 | "Link Globe", | |
| 391 | "Laptop", | |
| 392 | "Plane", | |
| 393 | "Car", | |
| 394 | "Binoculars", | |
| 395 | "Presentation", | |
| 396 | "Padlock", | |
| 397 | "Book (Open)", | |
| 398 | "Notebook Icon", | |
| 399 | "Paper (blank with lines)", | |
| 400 | "Research Icon", | |
| 401 | "Marker", | |
| 402 | "Dollar sign $", | |
| 403 | "Coins with window behind it", | |
| 404 | "Schedule Task", | |
| 405 | "Lighting Bolt", | |
| 406 | "Cloud", | |
| 407 | "Heart", | |
| 408 | "Flower", | |
| 409 | ]; | |
| 410 | ||
| 299 | 411 | #[derive(Clone, Debug, PartialEq)] |
| 300 | 412 | pub struct ParagraphTag { |
| 301 | 413 | pub icon: TagIcon, |
| ... | ... | @@ -367,6 +479,72 @@ fn caption(id: ExGuid, text_id: ExGuid, text: &str, format: Format) -> PageParag |
| 367 | 479 | } |
| 368 | 480 | } |
| 369 | 481 | |
| 482 | /// A file's column at `x`, before it is placed: its icon centred over its name, which OneNote | |
| 483 | /// shows without the extension; and the icon's height. | |
| 484 | fn file_column( | |
| 485 | file: &onestore::page::Attachment, | |
| 486 | paragraph: ExGuid, | |
| 487 | format: &Format, | |
| 488 | x: f32, | |
| 489 | shape: &mut impl FnMut( | |
| 490 | &PageParagraph, | |
| 491 | Option<&Count>, | |
| 492 | f32, | |
| 493 | &[f32], | |
| 494 | ) -> Result<ParagraphLayout, LayoutError>, | |
| 495 | ) -> Result<(ObjectLayout, f32), LayoutError> { | |
| 496 | let [w, h] = file.size.unwrap_or([24.0, 24.0]); | |
| 497 | let left = x + (ATTACHMENT_WIDTH - w) / 2.0; | |
| 498 | let name = std::path::Path::new(&file.filename) | |
| 499 | .file_stem() | |
| 500 | .and_then(|stem| stem.to_str()) | |
| 501 | .unwrap_or(&file.filename); | |
| 502 | let format = Format { | |
| 503 | alignment: Some(1), | |
| 504 | ..format.clone() | |
| 505 | }; | |
| 506 | let mut label = shape( | |
| 507 | &caption(paragraph, file.id, name, format), | |
| 508 | None, | |
| 509 | ATTACHMENT_WIDTH, | |
| 510 | &[0.0, 0.0], | |
| 511 | )?; | |
| 512 | label.reset_origin(x); | |
| 513 | let object = ObjectLayout { | |
| 514 | id: file.id, | |
| 515 | rect: [left, 0.0, left + w, 0.0], | |
| 516 | kind: ObjectKind::File(label), | |
| 517 | bottom: 0.0, | |
| 518 | }; | |
| 519 | Ok((object, h)) | |
| 520 | } | |
| 521 | ||
| 522 | /// A file on the page as OneNote draws one, relative to its position: the column a | |
| 523 | /// paragraph's file takes, with its text in the page's default format. | |
| 524 | pub(crate) fn page_file( | |
| 525 | engine: &mut TextEngine, | |
| 526 | file: &onestore::page::Attachment, | |
| 527 | ) -> Result<ObjectLayout, LayoutError> { | |
| 528 | let (mut object, height) = file_column( | |
| 529 | file, | |
| 530 | file.id, | |
| 531 | &Format::default(), | |
| 532 | 0.0, | |
| 533 | &mut |paragraph, previous, width, indents| { | |
| 534 | ParagraphLayout::shape( | |
| 535 | engine, | |
| 536 | paragraph, | |
| 537 | previous, | |
| 538 | width, | |
| 539 | indents, | |
| 540 | &BTreeMap::new(), | |
| 541 | ) | |
| 542 | }, | |
| 543 | )?; | |
| 544 | object.place(0.0, height); | |
| 545 | Ok(object) | |
| 546 | } | |
| 547 | ||
| 370 | 548 | /// A picture's displayed size: the user-set layout size, else its intrinsic size. |
| 371 | 549 | pub(crate) fn image_size(image: &onestore::page::Image) -> Option<[f32; 2]> { |
| 372 | 550 | let size = [ |
| ... | ... | @@ -887,7 +1065,7 @@ impl ParagraphLayout { |
| 887 | 1065 | for tag in &tag_definitions { |
| 888 | 1066 | let (tag, shape, label) = (tag.0, tag.1, tag.2); |
| 889 | 1067 | let icon = match shape { |
| 890 | None => TagIcon::Flag, | |
| 1068 | None => TagIcon::Task, | |
| 891 | 1069 | Some(shape) => match TagIcon::of(shape.unwrap_or(0), tag.status & 1 != 0) { |
| 892 | 1070 | Some(icon) => icon, |
| 893 | 1071 | None => continue, |
| ... | ... | @@ -1149,30 +1327,8 @@ impl OutlineLayout { |
| 1149 | 1327 | (object, h, x + w) |
| 1150 | 1328 | } |
| 1151 | 1329 | ParagraphContent::Attachment(file) => { |
| 1152 | let [w, h] = file.size.unwrap_or([24.0, 24.0]); | |
| 1153 | let left = x + (ATTACHMENT_WIDTH - w) / 2.0; | |
| 1154 | // OneNote shows the name without its extension. | |
| 1155 | let name = std::path::Path::new(&file.filename) | |
| 1156 | .file_stem() | |
| 1157 | .and_then(|stem| stem.to_str()) | |
| 1158 | .unwrap_or(&file.filename); | |
| 1159 | let format = Format { | |
| 1160 | alignment: Some(1), | |
| 1161 | ..node.format.clone() | |
| 1162 | }; | |
| 1163 | let mut label = shape( | |
| 1164 | &caption(node.id, file.id, name, format), | |
| 1165 | None, | |
| 1166 | ATTACHMENT_WIDTH, | |
| 1167 | &[0.0, 0.0], | |
| 1168 | )?; | |
| 1169 | label.reset_origin(x); | |
| 1170 | let object = ObjectLayout { | |
| 1171 | id: file.id, | |
| 1172 | rect: [left, 0.0, left + w, 0.0], | |
| 1173 | kind: ObjectKind::File(label), | |
| 1174 | bottom: 0.0, | |
| 1175 | }; | |
| 1330 | let (object, h) = | |
| 1331 | file_column(file, node.id, &node.format, x, shape)?; | |
| 1176 | 1332 | (object, h, x + ATTACHMENT_WIDTH) |
| 1177 | 1333 | } |
| 1178 | 1334 | ParagraphContent::Ink(ink) => { |
| ... | ... | @@ -2092,26 +2248,7 @@ mod tests { |
| 2092 | 2248 | task_id: None, |
| 2093 | 2249 | extra_set: 0, |
| 2094 | 2250 | }); |
| 2095 | for (shape, expected) in [ | |
| 2096 | ( | |
| 2097 | 3, | |
| 2098 | TagIcon::CheckBox { | |
| 2099 | checked: true, | |
| 2100 | mark: None, | |
| 2101 | }, | |
| 2102 | ), | |
| 2103 | (13, TagIcon::Star), | |
| 2104 | (15, TagIcon::Question), | |
| 2105 | (17, TagIcon::Exclamation), | |
| 2106 | (18, TagIcon::Phone), | |
| 2107 | (23, TagIcon::Address), | |
| 2108 | (118, TagIcon::Contact), | |
| 2109 | (136, TagIcon::Highlight), | |
| 2110 | (100, TagIcon::RedSquare), | |
| 2111 | (101, TagIcon::YellowSquare), | |
| 2112 | (102, TagIcon::BlueSquare), | |
| 2113 | (121, TagIcon::Music), | |
| 2114 | ] { | |
| 2251 | for shape in [3, 13, 15, 17, 18, 23, 118, 136, 100, 101, 102, 121] { | |
| 2115 | 2252 | definitions.insert( |
| 2116 | 2253 | id, |
| 2117 | 2254 | Definition { |
| ... | ... | @@ -2134,7 +2271,13 @@ mod tests { |
| 2134 | 2271 | &definitions, |
| 2135 | 2272 | ) |
| 2136 | 2273 | .unwrap(); |
| 2137 | assert_eq!(tagged.tags[0].icon, expected); | |
| 2274 | assert_eq!( | |
| 2275 | tagged.tags[0].icon, | |
| 2276 | TagIcon::Symbol { | |
| 2277 | shape, | |
| 2278 | checked: shape == 3 | |
| 2279 | } | |
| 2280 | ); | |
| 2138 | 2281 | let baseline = tagged.text.lines().next().unwrap().1.baseline; |
| 2139 | 2282 | assert_eq!( |
| 2140 | 2283 | tagged.tags[0].origin, |
| ... | ... | @@ -2203,12 +2346,18 @@ mod tests { |
| 2203 | 2346 | ) |
| 2204 | 2347 | .unwrap() |
| 2205 | 2348 | }; |
| 2206 | // A symbol without artwork shows as a plain tag, an Outlook task as its flag, and a | |
| 2207 | // tag whose definition is missing as nothing. | |
| 2208 | assert_eq!(shaped(&node, &definitions).tags[0].icon, TagIcon::Other); | |
| 2349 | // A symbol MS-ONE does not list keeps its number, an Outlook task shows its flag, and | |
| 2350 | // a tag whose definition is missing shows nothing. | |
| 2351 | assert_eq!( | |
| 2352 | shaped(&node, &definitions).tags[0].icon, | |
| 2353 | TagIcon::Symbol { | |
| 2354 | shape: 999, | |
| 2355 | checked: false | |
| 2356 | } | |
| 2357 | ); | |
| 2209 | 2358 | let mut task = node.clone(); |
| 2210 | 2359 | task.text_mut().unwrap().tags[0].status |= 4; |
| 2211 | assert_eq!(shaped(&task, &definitions).tags[0].icon, TagIcon::Flag); | |
| 2360 | assert_eq!(shaped(&task, &definitions).tags[0].icon, TagIcon::Task); | |
| 2212 | 2361 | let mut untagged = definitions.clone(); |
| 2213 | 2362 | untagged.remove(&id); |
| 2214 | 2363 | assert!(shaped(&node, &untagged).tags.is_empty()); |
| ... | ... | @@ -2431,6 +2580,7 @@ mod tests { |
| 2431 | 2580 | filename: "notes 🦀.txt".into(), |
| 2432 | 2581 | source_path: None, |
| 2433 | 2582 | size: Some([24.0, 24.0]), |
| 2583 | layout: Default::default(), | |
| 2434 | 2584 | bytes: None, |
| 2435 | 2585 | preview: None, |
| 2436 | 2586 | recording: None, |
| ... | ... | @@ -2542,7 +2692,10 @@ mod tests { |
| 2542 | 2692 | .iter() |
| 2543 | 2693 | .map(|tag| (tag.icon, tag.origin[0])) |
| 2544 | 2694 | .collect::<Vec<_>>(), |
| 2545 | [(TagIcon::RedSquare, -20.25), (TagIcon::Question, -8.25)] | |
| 2695 | [ | |
| 2696 | (TagIcon::of(100, false).unwrap(), -20.25), | |
| 2697 | (TagIcon::of(15, false).unwrap(), -8.25) | |
| 2698 | ] | |
| 2546 | 2699 | ); |
| 2547 | 2700 | let colors: Vec<_> = shaped |
| 2548 | 2701 | .text |
crates/canvas/src/template.rs+30| ... | ... | @@ -21,6 +21,36 @@ pub struct Template { |
| 21 | 21 | pub art: &'static [Art], |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | impl Template { | |
| 25 | /// The background pictures a page stores for this template: OneNote's places, our | |
| 26 | /// recreations' bytes. | |
| 27 | #[cfg(feature = "gpu")] | |
| 28 | pub fn pictures(&self) -> Result<Vec<onestore::page::Image>, onestore::page::text::EditError> { | |
| 29 | self.art | |
| 30 | .iter() | |
| 31 | .map(|art| { | |
| 32 | let bytes = crate::gpu::page::template_picture(art.art) | |
| 33 | .expect("Every template's art is recreated"); | |
| 34 | Ok(onestore::page::Image { | |
| 35 | id: onestore::page::text::new_id()?, | |
| 36 | layout: onestore::document::Layout { | |
| 37 | x: Some(art.position[0]), | |
| 38 | y: Some(art.position[1]), | |
| 39 | max_width: art.size.map(|size| size[0]), | |
| 40 | max_height: art.size.map(|size| size[1]), | |
| 41 | width_set_by_user: art.size.map(|_| true), | |
| 42 | ..Default::default() | |
| 43 | }, | |
| 44 | size: art.size, | |
| 45 | bytes: Some(bytes.into()), | |
| 46 | alt: None, | |
| 47 | background: true, | |
| 48 | }) | |
| 49 | }) | |
| 50 | .collect() | |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 24 | 54 | /// OneNote 2010's Decorative templates and Informal Meeting Notes (resources/onenote-templates.md), |
| 25 | 55 | /// in the order the gallery lists them. |
| 26 | 56 | pub const TEMPLATES: &[Template] = &[ |
crates/canvas/tests/attachment_insert.rs created+256| ... | ... | @@ -0,0 +1,256 @@ |
| 1 | //! Files attached through the editor, as Snowbound's Attach File and drops store them: the | |
| 2 | //! ops the editor records seal into a section that reads back with the exact bytes. | |
| 3 | //! `CANVAS_ATTACHMENT_EXPORT` names a directory receiving that section as a notebook, for | |
| 4 | //! a cold reopen in OneNote 2010 (`corpus/attachment-insert`), and `CANVAS_FLOATING_EXPORT` | |
| 5 | //! the section of files attached on blank page (`corpus/attachment-floating`). | |
| 6 | ||
| 7 | use canvas::{editor::CanvasEditor, layout::TextEngine}; | |
| 8 | use draw::edit::Movement; | |
| 9 | use onestore::{ | |
| 10 | Arena, Section, Store, | |
| 11 | op::{Edit, Op}, | |
| 12 | page::{Attachment, PageObject, ParagraphContent}, | |
| 13 | }; | |
| 14 | use std::sync::Arc; | |
| 15 | ||
| 16 | /// The icon OneNote 2010 stored with a text file (`corpus/native/cold-05-06-attachment`). | |
| 17 | fn native_icon() -> Arc<[u8]> { | |
| 18 | let native = | |
| 19 | include_bytes!("../../../corpus/native/cold-05-06-attachment/notebook/synthetic.one"); | |
| 20 | let arena = Arena::default(); | |
| 21 | let mut section = Section::open(&arena, native.to_vec()).unwrap(); | |
| 22 | let (space, ..) = section.pages().unwrap()[0].clone(); | |
| 23 | let page = section.page(space).unwrap(); | |
| 24 | page.objects | |
| 25 | .iter() | |
| 26 | .filter_map(|object| match object { | |
| 27 | PageObject::Outline(outline) => Some(&outline.paragraphs), | |
| 28 | _ => None, | |
| 29 | }) | |
| 30 | .flatten() | |
| 31 | .find_map(|paragraph| match &paragraph.content { | |
| 32 | ParagraphContent::Attachment(file) => file.preview.clone(), | |
| 33 | _ => None, | |
| 34 | }) | |
| 35 | .unwrap() | |
| 36 | } | |
| 37 | ||
| 38 | /// Two mebibytes no compressor shortens, larger than any buffer the writer fills at once. | |
| 39 | fn large() -> Vec<u8> { | |
| 40 | let mut state = 0x2545_f491_4f6c_dd1d_u64; | |
| 41 | (0..2 << 20) | |
| 42 | .map(|_| { | |
| 43 | state ^= state << 13; | |
| 44 | state ^= state >> 7; | |
| 45 | state ^= state << 17; | |
| 46 | state as u8 | |
| 47 | }) | |
| 48 | .collect() | |
| 49 | } | |
| 50 | ||
| 51 | fn file(name: &str, bytes: Vec<u8>, preview: Option<Arc<[u8]>>) -> Attachment { | |
| 52 | Attachment { | |
| 53 | id: onestore::page::text::new_id().unwrap(), | |
| 54 | filename: name.into(), | |
| 55 | source_path: Some(format!("/Users/snowbound/Documents/{name}")), | |
| 56 | size: Some([24.0, 24.0]), | |
| 57 | layout: Default::default(), | |
| 58 | bytes: Some(bytes.into()), | |
| 59 | preview, | |
| 60 | recording: None, | |
| 61 | } | |
| 62 | } | |
| 63 | ||
| 64 | #[test] | |
| 65 | fn attached_files_store_their_bytes_between_the_text_around_them() { | |
| 66 | let source = onestore::create_section("files.one", "Before the file", "Author").unwrap(); | |
| 67 | let arena = Arena::default(); | |
| 68 | let mut section = Section::open(&arena, source.clone()).unwrap(); | |
| 69 | let (space, ..) = section.pages().unwrap()[0].clone(); | |
| 70 | let mut engine = TextEngine::default(); | |
| 71 | let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap(); | |
| 72 | let body = editor | |
| 73 | .outlines() | |
| 74 | .iter() | |
| 75 | .find(|outline| !outline.title) | |
| 76 | .unwrap() | |
| 77 | .id; | |
| 78 | editor.focus_outline(body).unwrap(); | |
| 79 | editor | |
| 80 | .move_selection(&mut engine, Movement::DocumentEnd, false) | |
| 81 | .unwrap(); | |
| 82 | let small = file( | |
| 83 | "notes 🦀.txt", | |
| 84 | b"Snowbound attached this file.\n".to_vec(), | |
| 85 | Some(native_icon()), | |
| 86 | ); | |
| 87 | // The icon a host without the system's draws, which `PageView::insert_attachment` gives. | |
| 88 | let large = file( | |
| 89 | "large.bin", | |
| 90 | large(), | |
| 91 | Some(canvas::gpu::page::file_icon().into()), | |
| 92 | ); | |
| 93 | let mut at = 133_000_000_000_000_000; | |
| 94 | let mut store = |editor: &mut CanvasEditor| { | |
| 95 | let ops = editor.take_ops().unwrap(); | |
| 96 | assert!(!ops.is_empty()); | |
| 97 | at += 10_000_000; | |
| 98 | let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect(); | |
| 99 | section.apply("Author", &Edit { at, ops }).unwrap(); | |
| 100 | }; | |
| 101 | editor | |
| 102 | .insert_attachment(&mut engine, small.clone()) | |
| 103 | .unwrap(); | |
| 104 | store(&mut editor); | |
| 105 | editor.insert(&mut engine, "After the file").unwrap(); | |
| 106 | store(&mut editor); | |
| 107 | editor | |
| 108 | .insert_attachment(&mut engine, large.clone()) | |
| 109 | .unwrap(); | |
| 110 | store(&mut editor); | |
| 111 | let mut image = source; | |
| 112 | section.seal().unwrap().unwrap().apply(&mut image).unwrap(); | |
| 113 | ||
| 114 | let arena = Arena::default(); | |
| 115 | let reopened = Section::open(&arena, image.clone()).unwrap(); | |
| 116 | let page = reopened.page(space).unwrap(); | |
| 117 | let body = page | |
| 118 | .objects | |
| 119 | .iter() | |
| 120 | .find_map(|object| match object { | |
| 121 | PageObject::Outline(outline) if !outline.title => Some(outline), | |
| 122 | _ => None, | |
| 123 | }) | |
| 124 | .unwrap(); | |
| 125 | let shown: Vec<String> = body | |
| 126 | .paragraphs | |
| 127 | .iter() | |
| 128 | .map(|paragraph| match &paragraph.content { | |
| 129 | ParagraphContent::Text(text) => text.text.text().to_owned(), | |
| 130 | ParagraphContent::Attachment(file) => { | |
| 131 | let expected = [&small, &large] | |
| 132 | .into_iter() | |
| 133 | .find(|expected| expected.id == file.id) | |
| 134 | .unwrap(); | |
| 135 | assert_eq!(file, expected); | |
| 136 | assert_eq!(file.bytes, expected.bytes); | |
| 137 | assert_eq!(file.preview, expected.preview); | |
| 138 | format!("[{}]", file.filename) | |
| 139 | } | |
| 140 | _ => panic!(), | |
| 141 | }) | |
| 142 | .collect(); | |
| 143 | assert_eq!( | |
| 144 | shown, | |
| 145 | [ | |
| 146 | "Before the file", | |
| 147 | "[notes 🦀.txt]", | |
| 148 | "After the file", | |
| 149 | "[large.bin]", | |
| 150 | "" | |
| 151 | ] | |
| 152 | ); | |
| 153 | ||
| 154 | if let Some(directory) = std::env::var_os("CANVAS_ATTACHMENT_EXPORT") { | |
| 155 | let directory = std::path::PathBuf::from(directory); | |
| 156 | std::fs::create_dir_all(&directory).unwrap(); | |
| 157 | std::fs::write(directory.join("files.one"), &image).unwrap(); | |
| 158 | let file_id = Store::parse(&image).unwrap().header.file_id; | |
| 159 | std::fs::write( | |
| 160 | directory.join("Open Notebook.onetoc2"), | |
| 161 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("files.one", file_id)]) | |
| 162 | .unwrap(), | |
| 163 | ) | |
| 164 | .unwrap(); | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | /// Files attached where a click on blank page left the caret lie on the page there, as OneNote | |
| 169 | /// 2010 places them, and move as one does. | |
| 170 | #[test] | |
| 171 | fn files_attached_on_blank_page_store_their_bytes_where_they_were_placed_and_moved() { | |
| 172 | let source = onestore::create_section("files.one", "Floating files", "Author").unwrap(); | |
| 173 | let arena = Arena::default(); | |
| 174 | let mut section = Section::open(&arena, source.clone()).unwrap(); | |
| 175 | let (space, ..) = section.pages().unwrap()[0].clone(); | |
| 176 | let mut engine = TextEngine::default(); | |
| 177 | let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap(); | |
| 178 | let small = file( | |
| 179 | "float 🦀.txt", | |
| 180 | b"Snowbound put this file on the page.\n".to_vec(), | |
| 181 | Some(native_icon()), | |
| 182 | ); | |
| 183 | let large = file( | |
| 184 | "large.bin", | |
| 185 | large(), | |
| 186 | Some(canvas::gpu::page::file_icon().into()), | |
| 187 | ); | |
| 188 | let mut at = 133_000_000_000_000_000; | |
| 189 | let mut store = |editor: &mut CanvasEditor| { | |
| 190 | let ops = editor.take_ops().unwrap(); | |
| 191 | assert_eq!(ops.len(), 1, "{ops:?}"); | |
| 192 | at += 10_000_000; | |
| 193 | let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect(); | |
| 194 | section.apply("Author", &Edit { at, ops }).unwrap(); | |
| 195 | }; | |
| 196 | editor | |
| 197 | .place_caret(&mut engine, [342.0, 284.4], 240.0) | |
| 198 | .unwrap(); | |
| 199 | editor | |
| 200 | .insert_attachment(&mut engine, small.clone()) | |
| 201 | .unwrap(); | |
| 202 | store(&mut editor); | |
| 203 | editor | |
| 204 | .place_caret(&mut engine, [126.0, 410.4], 240.0) | |
| 205 | .unwrap(); | |
| 206 | editor | |
| 207 | .insert_attachment(&mut engine, large.clone()) | |
| 208 | .unwrap(); | |
| 209 | store(&mut editor); | |
| 210 | let (_, size) = editor.image_placement(large.id).unwrap(); | |
| 211 | editor | |
| 212 | .place_image(&mut engine, large.id, [288.0, 140.4], size) | |
| 213 | .unwrap(); | |
| 214 | store(&mut editor); | |
| 215 | let mut image = source; | |
| 216 | section.seal().unwrap().unwrap().apply(&mut image).unwrap(); | |
| 217 | ||
| 218 | let arena = Arena::default(); | |
| 219 | let reopened = Section::open(&arena, image.clone()).unwrap(); | |
| 220 | let page = reopened.page(space).unwrap(); | |
| 221 | let files: Vec<&Attachment> = page | |
| 222 | .objects | |
| 223 | .iter() | |
| 224 | .filter_map(|object| match object { | |
| 225 | PageObject::Attachment(file) => Some(file), | |
| 226 | _ => None, | |
| 227 | }) | |
| 228 | .collect(); | |
| 229 | let [first, second] = files.as_slice() else { | |
| 230 | panic!("{page:?}") | |
| 231 | }; | |
| 232 | for (stored, expected, at) in [ | |
| 233 | (first, &small, [342.0, 284.4]), | |
| 234 | (second, &large, [288.0, 140.4]), | |
| 235 | ] { | |
| 236 | assert_eq!(stored.id, expected.id); | |
| 237 | assert_eq!([stored.layout.x, stored.layout.y], at.map(Some)); | |
| 238 | assert_eq!(stored.layout.max_width, Some(54.0)); | |
| 239 | assert_eq!(stored.bytes, expected.bytes); | |
| 240 | assert_eq!(stored.preview, expected.preview); | |
| 241 | } | |
| 242 | assert_eq!(editor.page().unwrap(), page); | |
| 243 | ||
| 244 | if let Some(directory) = std::env::var_os("CANVAS_FLOATING_EXPORT") { | |
| 245 | let directory = std::path::PathBuf::from(directory); | |
| 246 | std::fs::create_dir_all(&directory).unwrap(); | |
| 247 | std::fs::write(directory.join("files.one"), &image).unwrap(); | |
| 248 | let file_id = Store::parse(&image).unwrap().header.file_id; | |
| 249 | std::fs::write( | |
| 250 | directory.join("Open Notebook.onetoc2"), | |
| 251 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("files.one", file_id)]) | |
| 252 | .unwrap(), | |
| 253 | ) | |
| 254 | .unwrap(); | |
| 255 | } | |
| 256 | } |
crates/canvas/tests/custom_tags.rs created+207| ... | ... | @@ -0,0 +1,207 @@ |
| 1 | //! Tags of a customized list, applied through the editor, store the definitions OneNote | |
| 2 | //! 2010's Customize Tags stores (`corpus/custom-tags`). | |
| 3 | ||
| 4 | use canvas::{ | |
| 5 | document::TextPosition, | |
| 6 | editor::{CanvasEditor, Formatting, NoteTag}, | |
| 7 | layout::TextEngine, | |
| 8 | }; | |
| 9 | use onestore::{RevisionIndex, Store, document::Document, op, page::Page}; | |
| 10 | ||
| 11 | fn page(bytes: &[u8]) -> (onestore::ExGuid, Page) { | |
| 12 | let store = Store::parse(bytes).unwrap(); | |
| 13 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 14 | let document = Document::parse(&index).unwrap(); | |
| 15 | let (space, _) = document.pages().unwrap()[0]; | |
| 16 | (space, Page::from_space(&document, space).unwrap()) | |
| 17 | } | |
| 18 | ||
| 19 | /// The list's tags: a symbol with both colours, a check box, a symbol alone and colours | |
| 20 | /// alone, at the places the definitions store as action types. | |
| 21 | fn list() -> Vec<NoteTag> { | |
| 22 | let tag = |label: &str, shape, color, highlight| NoteTag { | |
| 23 | label: label.into(), | |
| 24 | shape, | |
| 25 | color, | |
| 26 | highlight, | |
| 27 | }; | |
| 28 | vec![ | |
| 29 | tag("Snow check", 61, Some(0x0000_0080), Some(0x00ff_cc00)), | |
| 30 | tag("Snow task", 48, None, None), | |
| 31 | tag("Snow circle", 31, None, None), | |
| 32 | tag("Snow ink", 0, Some(0x00ff_0000), None), | |
| 33 | ] | |
| 34 | } | |
| 35 | ||
| 36 | /// Each paragraph takes the tags at these places in `list`; the third's box is checked. | |
| 37 | const TAGGED: [(&str, &[u16]); 5] = [ | |
| 38 | ("Custom symbol and colours", &[0]), | |
| 39 | ("Custom check box", &[1]), | |
| 40 | ("Custom check box, checked", &[1]), | |
| 41 | ("Two custom tags", &[2, 3]), | |
| 42 | // Typed before the new page's own "Untagged". | |
| 43 | ("", &[]), | |
| 44 | ]; | |
| 45 | ||
| 46 | /// `ONESTORE_CUSTOM_TAGS_EXPORT` names a new directory receiving the candidate for a cold | |
| 47 | /// reopen. | |
| 48 | #[test] | |
| 49 | fn custom_tags_store_what_customize_tags_stores() { | |
| 50 | let source = onestore::create_section("custom-tags.one", "Untagged", "Author").unwrap(); | |
| 51 | let (space, before) = page(&source); | |
| 52 | let mut engine = TextEngine::default(); | |
| 53 | let mut editor = CanvasEditor::from_page(before, &mut engine).unwrap(); | |
| 54 | let body = editor | |
| 55 | .outlines() | |
| 56 | .iter() | |
| 57 | .find(|outline| !outline.title) | |
| 58 | .unwrap() | |
| 59 | .id; | |
| 60 | editor.focus_outline(body).unwrap(); | |
| 61 | let at = |paragraph| { | |
| 62 | [TextPosition { | |
| 63 | paragraph, | |
| 64 | offset: 0, | |
| 65 | }; 2] | |
| 66 | .into() | |
| 67 | }; | |
| 68 | let list = list(); | |
| 69 | for (paragraph, (text, places)) in TAGGED.iter().enumerate() { | |
| 70 | if paragraph > 0 { | |
| 71 | editor.enter(&mut engine, false).unwrap(); | |
| 72 | } | |
| 73 | editor.insert(&mut engine, text).unwrap(); | |
| 74 | editor.select(at(paragraph)).unwrap(); | |
| 75 | for &place in *places { | |
| 76 | let tag = list[usize::from(place)].clone(); | |
| 77 | editor | |
| 78 | .format(&mut engine, Formatting::Tag(tag, place)) | |
| 79 | .unwrap(); | |
| 80 | } | |
| 81 | if paragraph == 2 { | |
| 82 | editor.format(&mut engine, Formatting::Check).unwrap(); | |
| 83 | } | |
| 84 | let offset = text.len() as u32; | |
| 85 | editor | |
| 86 | .select([TextPosition { paragraph, offset }; 2].into()) | |
| 87 | .unwrap(); | |
| 88 | } | |
| 89 | let arena = onestore::Arena::default(); | |
| 90 | let mut section = onestore::Section::open(&arena, source.to_vec()).unwrap(); | |
| 91 | let ops = editor | |
| 92 | .take_ops() | |
| 93 | .unwrap() | |
| 94 | .into_iter() | |
| 95 | .map(|op| op::Op::Page { space, op }) | |
| 96 | .collect(); | |
| 97 | section | |
| 98 | .apply( | |
| 99 | "Author", | |
| 100 | &op::Edit { | |
| 101 | at: 134_000_000_000_000_000, | |
| 102 | ops, | |
| 103 | }, | |
| 104 | ) | |
| 105 | .unwrap(); | |
| 106 | section.seal().unwrap(); | |
| 107 | let written = section.image(); | |
| 108 | let (_, stored) = page(&written); | |
| 109 | let mut definitions: Vec<_> = stored | |
| 110 | .definitions | |
| 111 | .values() | |
| 112 | .filter_map(|definition| NoteTag::of(&definition.kind)) | |
| 113 | .collect(); | |
| 114 | definitions.sort_by_key(|(_, action_type)| *action_type); | |
| 115 | let expected: Vec<_> = list.into_iter().zip(0..).collect(); | |
| 116 | assert_eq!(definitions, expected); | |
| 117 | let mut reread = CanvasEditor::from_page(stored, &mut engine).unwrap(); | |
| 118 | reread.focus_outline(body).unwrap(); | |
| 119 | for (paragraph, (_, places)) in TAGGED.iter().enumerate() { | |
| 120 | reread.select(at(paragraph)).unwrap(); | |
| 121 | let tags: Vec<u16> = reread | |
| 122 | .format_state() | |
| 123 | .unwrap() | |
| 124 | .tags | |
| 125 | .into_iter() | |
| 126 | .map(|(_, action_type)| action_type) | |
| 127 | .collect(); | |
| 128 | assert_eq!(tags, *places, "paragraph {paragraph}"); | |
| 129 | } | |
| 130 | if let Some(directory) = std::env::var_os("ONESTORE_CUSTOM_TAGS_EXPORT") { | |
| 131 | let directory = std::path::PathBuf::from(directory); | |
| 132 | std::fs::create_dir(&directory).unwrap(); | |
| 133 | std::fs::write(directory.join("custom-tags.one"), &written).unwrap(); | |
| 134 | let file_id = Store::parse(&written).unwrap().header.file_id; | |
| 135 | std::fs::write( | |
| 136 | directory.join("Open Notebook.onetoc2"), | |
| 137 | onestore::create_table_of_contents( | |
| 138 | "Open Notebook.onetoc2", | |
| 139 | &[("custom-tags.one", file_id)], | |
| 140 | ) | |
| 141 | .unwrap(), | |
| 142 | ) | |
| 143 | .unwrap(); | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | /// A tag OneNote 2010's New Tag made, applied from two places in its list, reads back with | |
| 148 | /// its name, symbol and colours (`corpus/custom-tags/native`). | |
| 149 | #[test] | |
| 150 | fn onenotes_custom_tags_read_back() { | |
| 151 | let bytes = include_bytes!("../../../corpus/custom-tags/native/shapes.one"); | |
| 152 | let store = Store::parse(bytes).unwrap(); | |
| 153 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 154 | let document = Document::parse(&index).unwrap(); | |
| 155 | let page = document | |
| 156 | .pages() | |
| 157 | .unwrap() | |
| 158 | .into_iter() | |
| 159 | .map(|(space, _)| Page::from_space(&document, space).unwrap()) | |
| 160 | .find(|page| page.title.starts_with("Custom tags")) | |
| 161 | .unwrap(); | |
| 162 | let mut engine = TextEngine::default(); | |
| 163 | let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap(); | |
| 164 | let body = editor | |
| 165 | .outlines() | |
| 166 | .iter() | |
| 167 | .find(|outline| !outline.title) | |
| 168 | .unwrap() | |
| 169 | .id; | |
| 170 | editor.focus_outline(body).unwrap(); | |
| 171 | let snow = NoteTag { | |
| 172 | label: "Snow check".into(), | |
| 173 | shape: 61, | |
| 174 | color: Some(0x0000_0080), | |
| 175 | highlight: Some(0x00ff_cc00), | |
| 176 | }; | |
| 177 | let to_do = NoteTag::defaults()[0].clone(); | |
| 178 | // Snow check went in at the top of the list, then moved below To Do. | |
| 179 | for (paragraph, expected) in [ | |
| 180 | (0, (snow.clone(), 0)), | |
| 181 | (1, (to_do.clone(), 1)), | |
| 182 | (2, (snow, 1)), | |
| 183 | (3, (to_do, 0)), | |
| 184 | ] { | |
| 185 | editor | |
| 186 | .select( | |
| 187 | [TextPosition { | |
| 188 | paragraph, | |
| 189 | offset: 0, | |
| 190 | }; 2] | |
| 191 | .into(), | |
| 192 | ) | |
| 193 | .unwrap(); | |
| 194 | assert_eq!(editor.format_state().unwrap().tags, [expected]); | |
| 195 | } | |
| 196 | let shaped = &editor.active_outline().shaped().paragraphs[0]; | |
| 197 | assert_eq!( | |
| 198 | shaped.tags[0].icon, | |
| 199 | canvas::outline::TagIcon::of(61, false).unwrap() | |
| 200 | ); | |
| 201 | assert!( | |
| 202 | shaped | |
| 203 | .text | |
| 204 | .backgrounds() | |
| 205 | .all(|(_, color)| color == 0x00ff_cc00) | |
| 206 | ); | |
| 207 | } |
crates/canvas/tests/ops_differential.rs+184-15| ... | ... | @@ -13,12 +13,21 @@ use canvas::{ |
| 13 | 13 | use draw::edit::Movement; |
| 14 | 14 | use onestore::{ |
| 15 | 15 | Arena, ExGuid, RevisionIndex, Section, Store, |
| 16 | document::{Document, Kind as Node}, | |
| 16 | document::{Document, Kind as Node, Layout}, | |
| 17 | 17 | op::{Edit, Op, PageOp}, |
| 18 | page::Page, | |
| 18 | page::{Image, Page}, | |
| 19 | 19 | }; |
| 20 | 20 | use std::{collections::BTreeMap, path::Path}; |
| 21 | 21 | |
| 22 | /// A one-pixel PNG standing in for template art's bytes. | |
| 23 | const ART: [u8; 67] = [ | |
| 24 | 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, | |
| 25 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, | |
| 26 | 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, | |
| 27 | 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, | |
| 28 | 0x42, 0x60, 0x82, | |
| 29 | ]; | |
| 30 | ||
| 22 | 31 | #[path = "../../onestore/tests/support/sweep.rs"] |
| 23 | 32 | mod sweep; |
| 24 | 33 | |
| ... | ... | @@ -98,11 +107,22 @@ enum Kind { |
| 98 | 107 | InsertSpace, |
| 99 | 108 | RemoveSpace, |
| 100 | 109 | InsertSpaceRight, |
| 110 | Paper, | |
| 111 | Art, | |
| 112 | Attach, | |
| 113 | /// A click on blank page, then Attach File: a file on the page. | |
| 114 | AttachPage, | |
| 115 | MoveFile, | |
| 116 | RemoveFile, | |
| 117 | /// Typing, backspaces and a word backspace at one caret: one undo step. | |
| 118 | TypeRun, | |
| 119 | /// More typing where the caret is, joining the run before it. | |
| 120 | TypeOn, | |
| 101 | 121 | Undo, |
| 102 | 122 | Redo, |
| 103 | 123 | } |
| 104 | 124 | |
| 105 | const KINDS: [Kind; 55] = [ | |
| 125 | const KINDS: [Kind; 57] = [ | |
| 106 | 126 | Kind::Type, |
| 107 | 127 | Kind::TypeEnd, |
| 108 | 128 | Kind::Compose, |
| ... | ... | @@ -156,10 +176,61 @@ const KINDS: [Kind; 55] = [ |
| 156 | 176 | Kind::InsertSpace, |
| 157 | 177 | Kind::RemoveSpace, |
| 158 | 178 | Kind::InsertSpaceRight, |
| 179 | Kind::Paper, | |
| 180 | Kind::Art, | |
| 159 | 181 | Kind::Undo, |
| 160 | 182 | Kind::Redo, |
| 161 | 183 | ]; |
| 162 | 184 | |
| 185 | /// Typing runs, in plans of their own so the random edits' sequences stay as they were. | |
| 186 | const TYPING_PLANS: [&[Kind]; 3] = [ | |
| 187 | &[Kind::TypeRun, Kind::Undo, Kind::Redo], | |
| 188 | &[ | |
| 189 | Kind::TypeRun, | |
| 190 | Kind::TypeOn, | |
| 191 | Kind::Undo, | |
| 192 | Kind::Redo, | |
| 193 | Kind::Undo, | |
| 194 | ], | |
| 195 | &[Kind::PageType, Kind::TypeOn, Kind::Undo, Kind::Redo], | |
| 196 | ]; | |
| 197 | ||
| 198 | /// Files attached at a random caret, in plans of their own so the random edits' sequences | |
| 199 | /// stay as they were. | |
| 200 | const ATTACH_PLANS: [&[Kind]; 2] = [ | |
| 201 | &[Kind::Attach, Kind::Undo, Kind::Redo], | |
| 202 | &[Kind::Attach, Kind::Type, Kind::Attach, Kind::Undo], | |
| 203 | ]; | |
| 204 | ||
| 205 | /// Files attached on blank page, moved, spaced and removed, in plans of their own so the | |
| 206 | /// random edits' sequences stay as they were. | |
| 207 | const FLOATING_PLANS: [&[Kind]; 4] = [ | |
| 208 | &[Kind::AttachPage, Kind::Undo, Kind::Redo], | |
| 209 | &[ | |
| 210 | Kind::AttachPage, | |
| 211 | Kind::MoveFile, | |
| 212 | Kind::Undo, | |
| 213 | Kind::Redo, | |
| 214 | Kind::RemoveFile, | |
| 215 | Kind::Undo, | |
| 216 | ], | |
| 217 | &[Kind::AttachPage, Kind::InsertSpace, Kind::Undo, Kind::Redo], | |
| 218 | &[ | |
| 219 | Kind::AttachPage, | |
| 220 | Kind::AttachPage, | |
| 221 | Kind::MoveFile, | |
| 222 | Kind::Undo, | |
| 223 | Kind::Undo, | |
| 224 | ], | |
| 225 | ]; | |
| 226 | ||
| 227 | /// Page colour, rule lines and art undone and redone across each other. | |
| 228 | const PAPER_PLANS: [&[Kind]; 3] = [ | |
| 229 | &[Kind::Art, Kind::Art, Kind::Undo, Kind::Undo, Kind::Redo], | |
| 230 | &[Kind::Art, Kind::Paper, Kind::Art, Kind::Undo, Kind::Undo], | |
| 231 | &[Kind::Paper, Kind::Art, Kind::Undo, Kind::Paper, Kind::Redo], | |
| 232 | ]; | |
| 233 | ||
| 163 | 234 | struct Random(u64); |
| 164 | 235 | |
| 165 | 236 | impl Random { |
| ... | ... | @@ -212,10 +283,13 @@ fn perform( |
| 212 | 283 | kind, |
| 213 | 284 | Kind::Undo |
| 214 | 285 | | Kind::Redo |
| 286 | | Kind::TypeOn | |
| 215 | 287 | | Kind::PlaceImage |
| 216 | 288 | | Kind::RemoveImage |
| 217 | 289 | | Kind::CreateOutline |
| 218 | 290 | | Kind::Date |
| 291 | | Kind::Paper | |
| 292 | | Kind::Art | |
| 219 | 293 | ) { |
| 220 | 294 | if outlines.is_empty() { |
| 221 | 295 | return false; |
| ... | ... | @@ -328,18 +402,9 @@ fn perform( |
| 328 | 402 | Kind::Bullets => format(editor, engine, random, Formatting::Bullets), |
| 329 | 403 | Kind::Numbering => format(editor, engine, random, Formatting::Numbering), |
| 330 | 404 | Kind::Tag => { |
| 331 | let tag = [ | |
| 332 | NoteTag::ToDo, | |
| 333 | NoteTag::Important, | |
| 334 | NoteTag::Question, | |
| 335 | NoteTag::RememberForLater, | |
| 336 | NoteTag::Definition, | |
| 337 | NoteTag::Highlight, | |
| 338 | NoteTag::Contact, | |
| 339 | NoteTag::Address, | |
| 340 | NoteTag::PhoneNumber, | |
| 341 | ][random.below(9)]; | |
| 342 | format(editor, engine, random, Formatting::Tag(tag)) | |
| 405 | let place = random.below(9); | |
| 406 | let tag = NoteTag::defaults().swap_remove(place); | |
| 407 | format(editor, engine, random, Formatting::Tag(tag, place as u16)) | |
| 343 | 408 | } |
| 344 | 409 | Kind::Check => format(editor, engine, random, Formatting::Check), |
| 345 | 410 | Kind::ClickCheck => { |
| ... | ... | @@ -462,6 +527,106 @@ fn perform( |
| 462 | 527 | let line = editor.active_outline().origin()[0] - random.below(40) as f32; |
| 463 | 528 | editor.insert_space(engine, 0, line, 45.0).unwrap_or(false) |
| 464 | 529 | } |
| 530 | // Lemon paper with college rules, or none. | |
| 531 | Kind::Paper => { | |
| 532 | let (color, lines) = match editor.page_color() { | |
| 533 | None => (Some(0x00dd_fdfd), Some(canvas::template::RULE_LINES[1].1)), | |
| 534 | Some(_) => (None, None), | |
| 535 | }; | |
| 536 | editor.set_paper(color, lines, None) | |
| 537 | } | |
| 538 | // Template art in two pieces, as the Page Color menu gives it, or none after art. | |
| 539 | Kind::Art => { | |
| 540 | let piece = |x: f32| Image { | |
| 541 | id: onestore::page::text::new_id().unwrap(), | |
| 542 | layout: Layout { | |
| 543 | x: Some(x), | |
| 544 | y: Some(-3.6), | |
| 545 | max_width: Some(174.5), | |
| 546 | max_height: Some(640.0), | |
| 547 | width_set_by_user: Some(true), | |
| 548 | ..Default::default() | |
| 549 | }, | |
| 550 | size: Some([174.5, 640.0]), | |
| 551 | bytes: Some(std::sync::Arc::from(ART.as_slice())), | |
| 552 | alt: None, | |
| 553 | background: true, | |
| 554 | }; | |
| 555 | let shown = editor.page().unwrap().objects.iter().any(|object| { | |
| 556 | matches!(object, onestore::page::PageObject::Image(image) if image.background) | |
| 557 | }); | |
| 558 | let art = match shown { | |
| 559 | true => Vec::new(), | |
| 560 | false => vec![piece(-27.0), piece(400.0)], | |
| 561 | }; | |
| 562 | editor.set_paper(editor.page_color(), editor.rule_lines(), Some(art)) | |
| 563 | } | |
| 564 | Kind::Attach => { | |
| 565 | let file = onestore::page::Attachment { | |
| 566 | id: onestore::page::text::new_id().unwrap(), | |
| 567 | filename: "notes.txt".into(), | |
| 568 | source_path: None, | |
| 569 | size: Some([24.0, 24.0]), | |
| 570 | layout: Default::default(), | |
| 571 | bytes: Some(std::sync::Arc::from(b"notes".as_slice())), | |
| 572 | preview: Some(std::sync::Arc::from(ART.as_slice())), | |
| 573 | recording: None, | |
| 574 | }; | |
| 575 | place(editor, middle) && editor.insert_attachment(engine, file).is_ok() | |
| 576 | } | |
| 577 | Kind::AttachPage => { | |
| 578 | let file = onestore::page::Attachment { | |
| 579 | id: onestore::page::text::new_id().unwrap(), | |
| 580 | filename: "float.txt".into(), | |
| 581 | source_path: None, | |
| 582 | size: Some([24.0, 24.0]), | |
| 583 | layout: Default::default(), | |
| 584 | bytes: Some(std::sync::Arc::from(b"float".as_slice())), | |
| 585 | preview: Some(std::sync::Arc::from(ART.as_slice())), | |
| 586 | recording: None, | |
| 587 | }; | |
| 588 | let x = 36.0 + 18.0 * random.below(20) as f32; | |
| 589 | editor.place_caret(engine, [x, 1440.0], 240.0).is_ok() | |
| 590 | && editor.insert_attachment(engine, file).is_ok() | |
| 591 | } | |
| 592 | Kind::MoveFile | Kind::RemoveFile => { | |
| 593 | let Some(file) = editor | |
| 594 | .page() | |
| 595 | .unwrap() | |
| 596 | .objects | |
| 597 | .into_iter() | |
| 598 | .find_map(|object| match object { | |
| 599 | onestore::page::PageObject::Attachment(file) => Some(file), | |
| 600 | _ => None, | |
| 601 | }) | |
| 602 | else { | |
| 603 | return false; | |
| 604 | }; | |
| 605 | if kind == Kind::RemoveFile { | |
| 606 | return editor.remove_image(engine, file.id).is_ok(); | |
| 607 | } | |
| 608 | let (origin, size) = editor.image_placement(file.id).unwrap(); | |
| 609 | let delta = [18.0, -36.0].map(|step| step * (1 + random.below(3)) as f32); | |
| 610 | editor | |
| 611 | .place_image( | |
| 612 | engine, | |
| 613 | file.id, | |
| 614 | [origin[0] + delta[0], origin[1] + delta[1]], | |
| 615 | size, | |
| 616 | ) | |
| 617 | .is_ok() | |
| 618 | } | |
| 619 | Kind::TypeRun => { | |
| 620 | place(editor, middle) | |
| 621 | && editor.insert(engine, "ab").is_ok() | |
| 622 | && editor.delete(engine, true).is_ok() | |
| 623 | && editor.insert(engine, " cd").is_ok() | |
| 624 | && editor.delete_to(engine, Movement::WordLeft).is_ok() | |
| 625 | && editor.commit_text(engine, "ef".into()).is_ok() | |
| 626 | } | |
| 627 | Kind::TypeOn => { | |
| 628 | editor.insert(engine, "gh").is_ok() && editor.delete(engine, true).is_ok() | |
| 629 | } | |
| 465 | 630 | Kind::Undo => editor.undo(engine).unwrap_or(false), |
| 466 | 631 | Kind::Redo => editor.redo(engine).unwrap_or(false), |
| 467 | 632 | } |
| ... | ... | @@ -749,6 +914,10 @@ fn sweep( |
| 749 | 914 | .map(|_| KINDS[random.below(KINDS.len())]) |
| 750 | 915 | .collect() |
| 751 | 916 | })) |
| 917 | .chain(PAPER_PLANS.map(<[Kind]>::to_vec)) | |
| 918 | .chain(ATTACH_PLANS.map(<[Kind]>::to_vec)) | |
| 919 | .chain(FLOATING_PLANS.map(<[Kind]>::to_vec)) | |
| 920 | .chain(TYPING_PLANS.map(<[Kind]>::to_vec)) | |
| 752 | 921 | .collect::<Vec<Vec<Kind>>>(); |
| 753 | 922 | for plan in plans { |
| 754 | 923 | let arena = Arena::default(); |
crates/canvas/tests/search.rs+2-1| ... | ... | @@ -235,7 +235,8 @@ fn tags_summary_lists_each_tagged_paragraph() { |
| 235 | 235 | // The probe's T24 and T25 also carry Call back, one entry under each tag. |
| 236 | 236 | assert_eq!(tagged.len(), names.len()); |
| 237 | 237 | names.dedup(); |
| 238 | let mut labels = canvas::editor::NoteTag::ALL.map(|tag| tag.label()); | |
| 238 | let defaults = canvas::editor::NoteTag::defaults(); | |
| 239 | let mut labels: Vec<&str> = defaults.iter().map(|tag| tag.label.as_str()).collect(); | |
| 239 | 240 | labels.sort_unstable(); |
| 240 | 241 | assert_eq!(names, labels); |
| 241 | 242 | assert!(tagged.iter().all(|tagged| tagged.space == space)); |
crates/draw/src/draw.glsl+31-1| ... | ... | @@ -10,6 +10,7 @@ attribute vec4 shape; |
| 10 | 10 | attribute float stroke; |
| 11 | 11 | attribute vec2 clip_local; |
| 12 | 12 | attribute vec3 clip; |
| 13 | attribute float blur; | |
| 13 | 14 | #endif |
| 14 | 15 | varying vec2 v_uv; |
| 15 | 16 | varying vec4 v_color; |
| ... | ... | @@ -18,6 +19,7 @@ varying vec4 v_shape; |
| 18 | 19 | varying float v_stroke; |
| 19 | 20 | varying vec2 v_clip_local; |
| 20 | 21 | varying vec3 v_clip; |
| 22 | varying float v_blur; | |
| 21 | 23 | |
| 22 | 24 | #ifdef VERTEX |
| 23 | 25 | void main() { |
| ... | ... | @@ -28,6 +30,7 @@ void main() { |
| 28 | 30 | v_stroke = stroke; |
| 29 | 31 | v_clip_local = clip_local; |
| 30 | 32 | v_clip = clip; |
| 33 | v_blur = blur; | |
| 31 | 34 | gl_Position = vec4(position, 0.0, 1.0); |
| 32 | 35 | } |
| 33 | 36 | #else |
| ... | ... | @@ -54,10 +57,37 @@ float ellipse_arc(float angle, vec2 radius) { |
| 54 | 57 | return half_angle * sum * scale; |
| 55 | 58 | } |
| 56 | 59 | |
| 60 | vec2 erf(vec2 x) { | |
| 61 | vec2 s = sign(x); | |
| 62 | vec2 a = abs(x); | |
| 63 | vec2 y = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; | |
| 64 | y *= y; | |
| 65 | return s - s / (y * y); | |
| 66 | } | |
| 67 | ||
| 68 | float shadow(vec2 p, vec2 half_size, float corner, float sigma) { | |
| 69 | float start = clamp(-3.0 * sigma, p.y - half_size.y, p.y + half_size.y); | |
| 70 | float end = clamp(3.0 * sigma, p.y - half_size.y, p.y + half_size.y); | |
| 71 | float stride = (end - start) / 4.0; | |
| 72 | float y = start + stride * 0.5; | |
| 73 | float value = 0.0; | |
| 74 | for (int i = 0; i < 4; i++) { | |
| 75 | float delta = min(half_size.y - corner - abs(p.y - y), 0.0); | |
| 76 | float curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); | |
| 77 | vec2 integral = 0.5 + 0.5 * erf((p.x + vec2(-curved, curved)) * (0.70710678 / sigma)); | |
| 78 | float weight = exp(-y * y / (2.0 * sigma * sigma)) / (2.50662827 * sigma); | |
| 79 | value += (integral.y - integral.x) * weight * stride; | |
| 80 | y += stride; | |
| 81 | } | |
| 82 | return value; | |
| 83 | } | |
| 84 | ||
| 57 | 85 | void main() { |
| 58 | 86 | vec4 color = texture2D(atlas, v_uv) * v_color; |
| 59 | 87 | vec4 shape = v_shape; |
| 60 | if (shape.x > 0.0 && shape.y > 0.0) { | |
| 88 | if (v_blur > 1e-6) { | |
| 89 | color *= shadow(v_local, shape.xy, shape.z, v_blur); | |
| 90 | } else if (shape.x > 0.0 && shape.y > 0.0) { | |
| 61 | 91 | vec2 q = abs(v_local) - shape.xy + shape.zw; |
| 62 | 92 | float distance; |
| 63 | 93 | if (same(shape.z, shape.w)) { |
crates/draw/src/draw.wgsl+35-3| ... | ... | @@ -7,6 +7,7 @@ struct Vertex { |
| 7 | 7 | @location(4) @interpolate(flat) stroke: f32, |
| 8 | 8 | @location(5) clip_local: vec2<f32>, |
| 9 | 9 | @location(6) @interpolate(flat) clip: vec3<f32>, |
| 10 | @location(7) @interpolate(flat) blur: f32, | |
| 10 | 11 | } |
| 11 | 12 | |
| 12 | 13 | @group(0) @binding(0) var atlas: texture_2d<f32>; |
| ... | ... | @@ -16,8 +17,9 @@ struct Vertex { |
| 16 | 17 | fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>, |
| 17 | 18 | @location(2) color: vec4<f32>, @location(3) local: vec2<f32>, |
| 18 | 19 | @location(4) shape: vec4<f32>, @location(5) stroke: f32, |
| 19 | @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>) -> Vertex { | |
| 20 | return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke, clip_local, clip); | |
| 20 | @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>, | |
| 21 | @location(8) blur: f32) -> Vertex { | |
| 22 | return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke, clip_local, clip, blur); | |
| 21 | 23 | } |
| 22 | 24 | |
| 23 | 25 | fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { |
| ... | ... | @@ -37,10 +39,40 @@ fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { |
| 37 | 39 | return half * sum * scale; |
| 38 | 40 | } |
| 39 | 41 | |
| 42 | fn erf(x: vec2<f32>) -> vec2<f32> { | |
| 43 | let s = sign(x); | |
| 44 | let a = abs(x); | |
| 45 | var y = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; | |
| 46 | y *= y; | |
| 47 | return s - s / (y * y); | |
| 48 | } | |
| 49 | ||
| 50 | // Coverage at `p` of a rectangle `half` about the origin with round corners `corner`, | |
| 51 | // blurred by a Gaussian of deviation `sigma`: exact across, sampled four times along | |
| 52 | // (Evan Wallace, "Fast Rounded Rectangle Shadows"). | |
| 53 | fn shadow(p: vec2<f32>, half: vec2<f32>, corner: f32, sigma: f32) -> f32 { | |
| 54 | let start = clamp(-3.0 * sigma, p.y - half.y, p.y + half.y); | |
| 55 | let end = clamp(3.0 * sigma, p.y - half.y, p.y + half.y); | |
| 56 | let stride = (end - start) / 4.0; | |
| 57 | var y = start + stride * 0.5; | |
| 58 | var value = 0.0; | |
| 59 | for (var i = 0; i < 4; i += 1) { | |
| 60 | let delta = min(half.y - corner - abs(p.y - y), 0.0); | |
| 61 | let curved = half.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); | |
| 62 | let integral = 0.5 + 0.5 * erf((p.x + vec2<f32>(-curved, curved)) * (0.70710678 / sigma)); | |
| 63 | let weight = exp(-y * y / (2.0 * sigma * sigma)) / (2.50662827 * sigma); | |
| 64 | value += (integral.y - integral.x) * weight * stride; | |
| 65 | y += stride; | |
| 66 | } | |
| 67 | return value; | |
| 68 | } | |
| 69 | ||
| 40 | 70 | @fragment |
| 41 | 71 | fn fragment(input: Vertex) -> @location(0) vec4<f32> { |
| 42 | 72 | var color = textureSample(atlas, atlas_sampler, input.uv) * input.color; |
| 43 | if input.shape.x > 0.0 && input.shape.y > 0.0 { | |
| 73 | if input.blur > 0.0 { | |
| 74 | color *= shadow(input.local, input.shape.xy, input.shape.z, input.blur); | |
| 75 | } else if input.shape.x > 0.0 && input.shape.y > 0.0 { | |
| 44 | 76 | let q = abs(input.local) - input.shape.xy + input.shape.zw; |
| 45 | 77 | var distance: f32; |
| 46 | 78 | if input.shape.z == input.shape.w { |
crates/draw/src/render.rs+184-41| ... | ... | @@ -32,7 +32,11 @@ use swash::{ |
| 32 | 32 | zeno::{Angle, Cap, Format, Join, Mask, Transform, Vector}, |
| 33 | 33 | }; |
| 34 | 34 | |
| 35 | /// The atlas's side at first; it doubles, up to `MAX_ATLAS_SIZE`, when one frame needs | |
| 36 | /// more than half of it. | |
| 35 | 37 | const ATLAS_SIZE: u32 = 2048; |
| 38 | const MAX_ATLAS_SIZE: u32 = 8192; | |
| 39 | /// Atlas entries per `ATLAS_SIZE` square of atlas. | |
| 36 | 40 | const MAX_GLYPHS: usize = 8192; |
| 37 | 41 | const MAX_VERTICES: usize = 65_536; |
| 38 | 42 | const VERTEX_BUFFER_BYTES: u64 = (MAX_VERTICES * size_of::<Vertex>()) as u64; |
| ... | ... | @@ -199,6 +203,8 @@ struct Vertex { |
| 199 | 203 | /// that rectangle's half size and corner radius; a zero half size paints everywhere. |
| 200 | 204 | clip_local: [f32; 2], |
| 201 | 205 | clip: [f32; 3], |
| 206 | /// The standard deviation, in device pixels, of the blur that makes `shape` a shadow. | |
| 207 | blur: f32, | |
| 202 | 208 | } |
| 203 | 209 | |
| 204 | 210 | #[derive(Hash, PartialEq, Eq)] |
| ... | ... | @@ -401,6 +407,14 @@ pub enum Primitive<'a> { |
| 401 | 407 | radius: [f32; 2], |
| 402 | 408 | colors: [[f32; 4]; 2], |
| 403 | 409 | }, |
| 410 | /// The soft shadow a rounded rectangle casts, blurred `blur` units wide as CSS's | |
| 411 | /// `box-shadow` takes it. | |
| 412 | Shadow { | |
| 413 | rect: [f32; 4], | |
| 414 | radius: f32, | |
| 415 | blur: f32, | |
| 416 | color: [f32; 4], | |
| 417 | }, | |
| 404 | 418 | Image { |
| 405 | 419 | image: &'a RasterImage, |
| 406 | 420 | rect: [f32; 4], |
| ... | ... | @@ -490,11 +504,36 @@ impl Renderer { |
| 490 | 504 | pen: [1, 0], |
| 491 | 505 | row_height: 1, |
| 492 | 506 | }; |
| 493 | // The atlas's first texel is the white that solid fills sample. | |
| 494 | 507 | renderer.write_atlas([0, 0], [1, 1], &[255; 4]); |
| 495 | 508 | renderer |
| 496 | 509 | } |
| 497 | 510 | |
| 511 | /// Replaces the atlas with an empty one twice as wide and tall. | |
| 512 | fn grow_atlas(&mut self) { | |
| 513 | self.new_atlas(self.atlas_side() * 2); | |
| 514 | self.clear_glyph_cache(); | |
| 515 | self.write_atlas([0, 0], [1, 1], &[255; 4]); | |
| 516 | } | |
| 517 | ||
| 518 | fn glyph_limit(&self) -> usize { | |
| 519 | MAX_GLYPHS * (self.atlas_side() / ATLAS_SIZE).pow(2) as usize | |
| 520 | } | |
| 521 | ||
| 522 | fn uv(&self, glyph: AtlasGlyph) -> [f32; 4] { | |
| 523 | let side = self.atlas_side() as f32; | |
| 524 | [ | |
| 525 | glyph.x as f32 / side, | |
| 526 | glyph.y as f32 / side, | |
| 527 | (glyph.x + glyph.width) as f32 / side, | |
| 528 | (glyph.y + glyph.height) as f32 / side, | |
| 529 | ] | |
| 530 | } | |
| 531 | ||
| 532 | /// The atlas's first texel, the white that solid fills sample. | |
| 533 | fn white(&self) -> [f32; 4] { | |
| 534 | [0.5 / self.atlas_side() as f32; 4] | |
| 535 | } | |
| 536 | ||
| 498 | 537 | pub fn clear_glyph_cache(&mut self) { |
| 499 | 538 | self.glyphs.clear(); |
| 500 | 539 | self.pen = [1, 0]; |
| ... | ... | @@ -524,9 +563,9 @@ impl Renderer { |
| 524 | 563 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), |
| 525 | 564 | glyph_capacity: self.glyphs.capacity(), |
| 526 | 565 | image_capacity: self.images.capacity(), |
| 527 | atlas_bytes: u64::from(ATLAS_SIZE) * u64::from(ATLAS_SIZE) * 4, | |
| 566 | atlas_bytes: u64::from(self.atlas_side()).pow(2) * 4, | |
| 528 | 567 | vertex_buffer_bytes: VERTEX_BUFFER_BYTES, |
| 529 | within_budget: self.glyphs.len() <= MAX_GLYPHS | |
| 568 | within_budget: self.glyphs.len() <= self.glyph_limit() | |
| 530 | 569 | && self.images.len() <= MAX_IMAGES |
| 531 | 570 | && image_bytes <= MAX_IMAGE_BYTES |
| 532 | 571 | && self.vertices.len() <= MAX_VERTICES, |
| ... | ... | @@ -578,17 +617,35 @@ impl Renderer { |
| 578 | 617 | } |
| 579 | 618 | } |
| 580 | 619 | } |
| 581 | for attempt in 0..2 { | |
| 620 | let limit = self.max_texture_dimension().min(MAX_ATLAS_SIZE); | |
| 621 | let mut cleared = false; | |
| 622 | loop { | |
| 582 | 623 | self.vertices.clear(); |
| 583 | 624 | self.batches.clear(); |
| 584 | 625 | self.groups.clear(); |
| 585 | 626 | self.barrier = 0; |
| 586 | match self.prepare(size, layers, &active_images) { | |
| 587 | Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(), | |
| 627 | let full = match self.prepare(size, layers, &active_images) { | |
| 628 | Err(RenderError::AtlasFull) => true, | |
| 588 | 629 | result => { |
| 589 | 630 | result?; |
| 590 | break; | |
| 631 | false | |
| 591 | 632 | } |
| 633 | }; | |
| 634 | // Once cleared, the atlas holds only this frame's entries; past half full, the | |
| 635 | // next frames would clear it again and again. | |
| 636 | let crowded = full || (cleared && 2 * self.pen[1] > self.atlas_side()); | |
| 637 | if !crowded { | |
| 638 | break; | |
| 639 | } | |
| 640 | if !cleared { | |
| 641 | self.clear_glyph_cache(); | |
| 642 | cleared = true; | |
| 643 | } else if self.atlas_side() * 2 <= limit { | |
| 644 | self.grow_atlas(); | |
| 645 | } else if full { | |
| 646 | return Err(RenderError::AtlasFull); | |
| 647 | } else { | |
| 648 | break; | |
| 592 | 649 | } |
| 593 | 650 | } |
| 594 | 651 | self.submit(target, size, clear); |
| ... | ... | @@ -677,6 +734,7 @@ impl Renderer { |
| 677 | 734 | stroke: 0.0, |
| 678 | 735 | clip_local: [0.0; 2], |
| 679 | 736 | clip: [0.0; 3], |
| 737 | blur: 0.0, | |
| 680 | 738 | } |
| 681 | 739 | }; |
| 682 | 740 | for strip in 0..strips { |
| ... | ... | @@ -766,7 +824,7 @@ impl Renderer { |
| 766 | 824 | rect[2] * space.scale + space.origin[0], |
| 767 | 825 | rect[3] * space.scale + space.origin[1], |
| 768 | 826 | ], |
| 769 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 827 | self.white(), | |
| 770 | 828 | *color, |
| 771 | 829 | )?; |
| 772 | 830 | } |
| ... | ... | @@ -781,6 +839,12 @@ impl Renderer { |
| 781 | 839 | radius, |
| 782 | 840 | colors, |
| 783 | 841 | } => self.rounded_rect(space, *rect, *radius, None, *colors)?, |
| 842 | Primitive::Shadow { | |
| 843 | rect, | |
| 844 | radius, | |
| 845 | blur, | |
| 846 | color, | |
| 847 | } => self.shadow(space, *rect, *radius, *blur, *color)?, | |
| 784 | 848 | Primitive::Segment { |
| 785 | 849 | from, |
| 786 | 850 | to, |
| ... | ... | @@ -871,7 +935,7 @@ impl Renderer { |
| 871 | 935 | return Ok(()); |
| 872 | 936 | } |
| 873 | 937 | let size = run.size * scale; |
| 874 | if !size.is_finite() || size > ATLAS_SIZE as f32 { | |
| 938 | if !size.is_finite() || size > self.atlas_side() as f32 { | |
| 875 | 939 | return Err(RenderError::AtlasFull); |
| 876 | 940 | } |
| 877 | 941 | let color = run |
| ... | ... | @@ -905,7 +969,7 @@ impl Renderer { |
| 905 | 969 | let cached = if let Some(cached) = self.glyphs.get(&key) { |
| 906 | 970 | *cached |
| 907 | 971 | } else { |
| 908 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 972 | if self.glyphs.len() >= self.glyph_limit() { | |
| 909 | 973 | return Err(RenderError::AtlasFull); |
| 910 | 974 | } |
| 911 | 975 | let font = FontRef::from_index(run.font.as_ref(), run.index as usize) |
| ... | ... | @@ -967,16 +1031,10 @@ impl Renderer { |
| 967 | 1031 | height *= ratio; |
| 968 | 1032 | top = top.clamp(line_top, (line_top + line_height - height).max(line_top)); |
| 969 | 1033 | } |
| 970 | let atlas_size = ATLAS_SIZE as f32; | |
| 971 | 1034 | self.quad( |
| 972 | 1035 | space, |
| 973 | 1036 | [left, top, left + width, top + height], |
| 974 | [ | |
| 975 | glyph.x as f32 / atlas_size, | |
| 976 | glyph.y as f32 / atlas_size, | |
| 977 | (glyph.x + glyph.width) as f32 / atlas_size, | |
| 978 | (glyph.y + glyph.height) as f32 / atlas_size, | |
| 979 | ], | |
| 1037 | self.uv(glyph), | |
| 980 | 1038 | if glyph.color { [1.0; 4] } else { color }, |
| 981 | 1039 | )?; |
| 982 | 1040 | } |
| ... | ... | @@ -992,7 +1050,7 @@ impl Renderer { |
| 992 | 1050 | x + decoration.width * scale, |
| 993 | 1051 | y + (decoration.thickness * scale).max(1.0), |
| 994 | 1052 | ], |
| 995 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 1053 | self.white(), | |
| 996 | 1054 | decoration |
| 997 | 1055 | .color |
| 998 | 1056 | .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)), |
| ... | ... | @@ -1003,15 +1061,16 @@ impl Renderer { |
| 1003 | 1061 | |
| 1004 | 1062 | fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> { |
| 1005 | 1063 | let p = image.placement; |
| 1006 | if p.width + 2 > ATLAS_SIZE || p.height + 2 > ATLAS_SIZE { | |
| 1064 | let side = self.atlas_side(); | |
| 1065 | if p.width + 2 > side || p.height + 2 > side { | |
| 1007 | 1066 | return Err(RenderError::AtlasFull); |
| 1008 | 1067 | } |
| 1009 | if self.pen[0] + p.width + 1 > ATLAS_SIZE { | |
| 1068 | if self.pen[0] + p.width + 1 > side { | |
| 1010 | 1069 | self.pen[0] = 0; |
| 1011 | 1070 | self.pen[1] += self.row_height + 1; |
| 1012 | 1071 | self.row_height = 0; |
| 1013 | 1072 | } |
| 1014 | if self.pen[1] + p.height + 1 > ATLAS_SIZE { | |
| 1073 | if self.pen[1] + p.height + 1 > side { | |
| 1015 | 1074 | return Err(RenderError::AtlasFull); |
| 1016 | 1075 | } |
| 1017 | 1076 | let cached = AtlasGlyph { |
| ... | ... | @@ -1057,7 +1116,7 @@ impl Renderer { |
| 1057 | 1116 | { |
| 1058 | 1117 | return Ok(()); |
| 1059 | 1118 | } |
| 1060 | if size + 3.0 > ATLAS_SIZE as f32 { | |
| 1119 | if size + 3.0 > self.atlas_side() as f32 { | |
| 1061 | 1120 | return Err(RenderError::AtlasFull); |
| 1062 | 1121 | } |
| 1063 | 1122 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; |
| ... | ... | @@ -1072,7 +1131,7 @@ impl Renderer { |
| 1072 | 1131 | let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) { |
| 1073 | 1132 | *glyph |
| 1074 | 1133 | } else { |
| 1075 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 1134 | if self.glyphs.len() >= self.glyph_limit() { | |
| 1076 | 1135 | return Err(RenderError::AtlasFull); |
| 1077 | 1136 | } |
| 1078 | 1137 | let glyph = self.upload(icon::rasterize(sources, size, phase, ink, palette))?; |
| ... | ... | @@ -1081,7 +1140,6 @@ impl Renderer { |
| 1081 | 1140 | }; |
| 1082 | 1141 | let left = x.floor(); |
| 1083 | 1142 | let top = y.floor(); |
| 1084 | let atlas = ATLAS_SIZE as f32; | |
| 1085 | 1143 | self.quad( |
| 1086 | 1144 | space, |
| 1087 | 1145 | [ |
| ... | ... | @@ -1090,12 +1148,7 @@ impl Renderer { |
| 1090 | 1148 | left + glyph.width as f32, |
| 1091 | 1149 | top + glyph.height as f32, |
| 1092 | 1150 | ], |
| 1093 | [ | |
| 1094 | glyph.x as f32 / atlas, | |
| 1095 | glyph.y as f32 / atlas, | |
| 1096 | (glyph.x + glyph.width) as f32 / atlas, | |
| 1097 | (glyph.y + glyph.height) as f32 / atlas, | |
| 1098 | ], | |
| 1151 | self.uv(glyph), | |
| 1099 | 1152 | [1.0, 1.0, 1.0, tint[3]], |
| 1100 | 1153 | ) |
| 1101 | 1154 | } |
| ... | ... | @@ -1133,7 +1186,7 @@ impl Renderer { |
| 1133 | 1186 | let start = self.vertices.len(); |
| 1134 | 1187 | // A pixel of margin holds the antialiased fringe outside the edge. |
| 1135 | 1188 | let fringe = [rect[0] - 1.0, rect[1] - 1.0, rect[2] + 1.0, rect[3] + 1.0]; |
| 1136 | self.quad(space, fringe, [0.5 / ATLAS_SIZE as f32; 4], colors[0])?; | |
| 1189 | self.quad(space, fringe, self.white(), colors[0])?; | |
| 1137 | 1190 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; |
| 1138 | 1191 | if !(4.0 * (half[0] + half[1])).is_finite() || !colors[1].iter().all(|v| v.is_finite()) { |
| 1139 | 1192 | return Err(RenderError::InvalidPrimitive); |
| ... | ... | @@ -1160,6 +1213,41 @@ impl Renderer { |
| 1160 | 1213 | Ok(()) |
| 1161 | 1214 | } |
| 1162 | 1215 | |
| 1216 | fn shadow( | |
| 1217 | &mut self, | |
| 1218 | space: Space, | |
| 1219 | rect: [f32; 4], | |
| 1220 | radius: f32, | |
| 1221 | blur: f32, | |
| 1222 | color: [f32; 4], | |
| 1223 | ) -> Result<(), RenderError> { | |
| 1224 | if !radius.is_finite() || radius < 0.0 || !blur.is_finite() || blur <= 0.0 { | |
| 1225 | return Err(RenderError::InvalidPrimitive); | |
| 1226 | } | |
| 1227 | let rect = [0, 1, 2, 3].map(|i| rect[i] * space.scale + space.origin[i % 2]); | |
| 1228 | let sigma = blur * space.scale / 2.0; | |
| 1229 | let reach = 3.0 * sigma; | |
| 1230 | let start = self.vertices.len(); | |
| 1231 | self.quad( | |
| 1232 | space, | |
| 1233 | [ | |
| 1234 | rect[0] - reach, | |
| 1235 | rect[1] - reach, | |
| 1236 | rect[2] + reach, | |
| 1237 | rect[3] + reach, | |
| 1238 | ], | |
| 1239 | self.white(), | |
| 1240 | color, | |
| 1241 | )?; | |
| 1242 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | |
| 1243 | let radius = (radius * space.scale).min(half[0]).min(half[1]).max(0.0); | |
| 1244 | for vertex in &mut self.vertices[start..] { | |
| 1245 | vertex.shape = [half[0], half[1], radius, radius]; | |
| 1246 | vertex.blur = sigma; | |
| 1247 | } | |
| 1248 | Ok(()) | |
| 1249 | } | |
| 1250 | ||
| 1163 | 1251 | fn path( |
| 1164 | 1252 | &mut self, |
| 1165 | 1253 | space: Space, |
| ... | ... | @@ -1195,7 +1283,7 @@ impl Renderer { |
| 1195 | 1283 | let glyph = match self.glyphs.get(&key) { |
| 1196 | 1284 | Some(glyph) => *glyph, |
| 1197 | 1285 | None => { |
| 1198 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 1286 | if self.glyphs.len() >= self.glyph_limit() { | |
| 1199 | 1287 | return Err(RenderError::AtlasFull); |
| 1200 | 1288 | } |
| 1201 | 1289 | let mut mask = Mask::new(data); |
| ... | ... | @@ -1232,7 +1320,6 @@ impl Renderer { |
| 1232 | 1320 | }; |
| 1233 | 1321 | let left = x.floor() + glyph.left as f32; |
| 1234 | 1322 | let top = y.floor() + glyph.top as f32; |
| 1235 | let atlas = ATLAS_SIZE as f32; | |
| 1236 | 1323 | let start = self.vertices.len(); |
| 1237 | 1324 | self.quad( |
| 1238 | 1325 | space, |
| ... | ... | @@ -1242,12 +1329,7 @@ impl Renderer { |
| 1242 | 1329 | left + glyph.width as f32, |
| 1243 | 1330 | top + glyph.height as f32, |
| 1244 | 1331 | ], |
| 1245 | [ | |
| 1246 | glyph.x as f32 / atlas, | |
| 1247 | glyph.y as f32 / atlas, | |
| 1248 | (glyph.x + glyph.width) as f32 / atlas, | |
| 1249 | (glyph.y + glyph.height) as f32 / atlas, | |
| 1250 | ], | |
| 1332 | self.uv(glyph), | |
| 1251 | 1333 | colors[0], |
| 1252 | 1334 | )?; |
| 1253 | 1335 | for vertex in &mut self.vertices[start..] { |
| ... | ... | @@ -1318,13 +1400,14 @@ impl Renderer { |
| 1318 | 1400 | x * 2.0 / space.size[0] as f32 - 1.0, |
| 1319 | 1401 | 1.0 - y * 2.0 / space.size[1] as f32, |
| 1320 | 1402 | ], |
| 1321 | uv: [0.5 / ATLAS_SIZE as f32; 2], | |
| 1403 | uv: [0.5 / self.atlas_side() as f32; 2], | |
| 1322 | 1404 | color, |
| 1323 | 1405 | local, |
| 1324 | 1406 | shape: [half[0], half[1], corner, corner], |
| 1325 | 1407 | stroke: 0.0, |
| 1326 | 1408 | clip_local: [0.0; 2], |
| 1327 | 1409 | clip: [0.0; 3], |
| 1410 | blur: 0.0, | |
| 1328 | 1411 | }); |
| 1329 | 1412 | } |
| 1330 | 1413 | Ok(()) |
| ... | ... | @@ -1378,6 +1461,7 @@ impl Renderer { |
| 1378 | 1461 | stroke: 0.0, |
| 1379 | 1462 | clip_local: [0.0; 2], |
| 1380 | 1463 | clip: [0.0; 3], |
| 1464 | blur: 0.0, | |
| 1381 | 1465 | }); |
| 1382 | 1466 | } |
| 1383 | 1467 | Ok(()) |
| ... | ... | @@ -2031,7 +2115,7 @@ mod tests { |
| 2031 | 2115 | ); |
| 2032 | 2116 | } |
| 2033 | 2117 | assert!(!renderer.glyphs.is_empty()); |
| 2034 | assert!(renderer.glyphs.len() <= MAX_GLYPHS); | |
| 2118 | assert!(renderer.glyphs.len() <= renderer.glyph_limit()); | |
| 2035 | 2119 | assert_eq!( |
| 2036 | 2120 | renderer |
| 2037 | 2121 | .glyphs |
| ... | ... | @@ -2255,6 +2339,12 @@ mod tests { |
| 2255 | 2339 | style: PathStyle::Shadow(6.0), |
| 2256 | 2340 | colors: [[0.0, 0.0, 0.0, 1.0]; 2], |
| 2257 | 2341 | }, |
| 2342 | Primitive::Shadow { | |
| 2343 | rect: [170.0, 90.0, 190.0, 110.0], | |
| 2344 | radius: 4.0, | |
| 2345 | blur: 6.0, | |
| 2346 | color: [0.0, 0.0, 0.0, 1.0], | |
| 2347 | }, | |
| 2258 | 2348 | Primitive::Gradient { |
| 2259 | 2349 | rect: [100.0, 10.0, 120.0, 110.0], |
| 2260 | 2350 | radius: [0.0; 2], |
| ... | ... | @@ -2305,6 +2395,59 @@ mod tests { |
| 2305 | 2395 | "and fades at its edge: {fading:?}" |
| 2306 | 2396 | ); |
| 2307 | 2397 | assert_eq!(beyond, [255; 4]); |
| 2398 | let [inside, fading, beyond] = [pixel(360, 200), pixel(340, 200), pixel(310, 200)]; | |
| 2399 | assert!( | |
| 2400 | inside[0] < 40, | |
| 2401 | "a rectangle's shadow is dark inside: {inside:?}" | |
| 2402 | ); | |
| 2403 | assert!( | |
| 2404 | fading[0] > 40 && fading[0] < 230, | |
| 2405 | "and fades at its edge: {fading:?}" | |
| 2406 | ); | |
| 2407 | assert_eq!(beyond, [255; 4]); | |
| 2408 | assert_eq!(renderer.glyphs.len(), 3, "and takes no room in the atlas"); | |
| 2409 | } | |
| 2410 | ||
| 2411 | #[test] | |
| 2412 | #[ignore = "requires a native GPU adapter"] | |
| 2413 | fn icons_past_one_atlas_never_fail_a_frame() { | |
| 2414 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 2415 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 2416 | let (device, queue) = | |
| 2417 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 2418 | let target = Target::new(&device); | |
| 2419 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; | |
| 2420 | let mut renderer = Renderer::new(device, queue, format); | |
| 2421 | // Each tint is an entry of its own, 64 device pixels square. | |
| 2422 | let icons = |tints: Range<usize>| -> Vec<Primitive<'static>> { | |
| 2423 | tints | |
| 2424 | .map(|tint| Primitive::Icon { | |
| 2425 | sources: &[CHECKBOX], | |
| 2426 | origin: [0.0; 2], | |
| 2427 | size: 32.0, | |
| 2428 | tint: [tint as f32 / 65_536.0, 0.0, 0.0, 1.0], | |
| 2429 | palette: Palette::default(), | |
| 2430 | }) | |
| 2431 | .collect() | |
| 2432 | }; | |
| 2433 | let one_atlas = (ATLAS_SIZE as usize / 65).pow(2); | |
| 2434 | for frame in 0..8 { | |
| 2435 | let start = frame * one_atlas / 3; | |
| 2436 | let primitives = icons(start..start + one_atlas / 3); | |
| 2437 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 2438 | } | |
| 2439 | assert_eq!( | |
| 2440 | renderer.atlas_side(), | |
| 2441 | ATLAS_SIZE, | |
| 2442 | "frames each needing a third of the atlas evict, never grow it" | |
| 2443 | ); | |
| 2444 | let primitives = icons(0..3 * one_atlas); | |
| 2445 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 2446 | assert!(renderer.atlas_side() > ATLAS_SIZE); | |
| 2447 | assert!(renderer.glyphs.len() >= 3 * one_atlas); | |
| 2448 | let capture = target.capture(&renderer); | |
| 2449 | let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 2450 | assert_ne!(pixel(24 + 12, 24 + 32), [255; 4], "the icons paint"); | |
| 2308 | 2451 | } |
| 2309 | 2452 | |
| 2310 | 2453 | #[test] |
crates/draw/src/render/gl.rs+10-1| ... | ... | @@ -305,7 +305,7 @@ pub(super) struct Gpu { |
| 305 | 305 | } |
| 306 | 306 | |
| 307 | 307 | /// Each attribute's name, component count and offset, bound to its index in the shader. |
| 308 | const ATTRIBUTES: [(&CStr, GLint, usize); 8] = [ | |
| 308 | const ATTRIBUTES: [(&CStr, GLint, usize); 9] = [ | |
| 309 | 309 | (c"position", 2, offset_of!(Vertex, position)), |
| 310 | 310 | (c"uv", 2, offset_of!(Vertex, uv)), |
| 311 | 311 | (c"color", 4, offset_of!(Vertex, color)), |
| ... | ... | @@ -314,6 +314,7 @@ const ATTRIBUTES: [(&CStr, GLint, usize); 8] = [ |
| 314 | 314 | (c"stroke", 1, offset_of!(Vertex, stroke)), |
| 315 | 315 | (c"clip_local", 2, offset_of!(Vertex, clip_local)), |
| 316 | 316 | (c"clip", 3, offset_of!(Vertex, clip)), |
| 317 | (c"blur", 1, offset_of!(Vertex, blur)), | |
| 317 | 318 | ]; |
| 318 | 319 | |
| 319 | 320 | impl Renderer { |
| ... | ... | @@ -384,6 +385,14 @@ impl Renderer { |
| 384 | 385 | self.gpu.max_texture |
| 385 | 386 | } |
| 386 | 387 | |
| 388 | pub(super) fn atlas_side(&self) -> u32 { | |
| 389 | self.gpu.atlas.size[0] | |
| 390 | } | |
| 391 | ||
| 392 | pub(super) fn new_atlas(&mut self, side: u32) { | |
| 393 | self.gpu.atlas = Image::new([side; 2], NEAREST, None); | |
| 394 | } | |
| 395 | ||
| 387 | 396 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { |
| 388 | 397 | unsafe { |
| 389 | 398 | glBindTexture(TEXTURE_2D, self.gpu.atlas.name); |
crates/draw/src/render/webgpu.rs+53-35| ... | ... | @@ -63,7 +63,7 @@ impl Renderer { |
| 63 | 63 | buffers: &[Some(wgpu::VertexBufferLayout { |
| 64 | 64 | array_stride: size_of::<Vertex>() as u64, |
| 65 | 65 | step_mode: wgpu::VertexStepMode::Vertex, |
| 66 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32, 6 => Float32x2, 7 => Float32x3], | |
| 66 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32, 6 => Float32x2, 7 => Float32x3, 8 => Float32], | |
| 67 | 67 | })], |
| 68 | 68 | }, |
| 69 | 69 | fragment: Some(wgpu::FragmentState { |
| ... | ... | @@ -98,40 +98,7 @@ impl Renderer { |
| 98 | 98 | color: erase, |
| 99 | 99 | alpha: erase, |
| 100 | 100 | }); |
| 101 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | |
| 102 | label: Some("Glyph atlas"), | |
| 103 | size: wgpu::Extent3d { | |
| 104 | width: ATLAS_SIZE, | |
| 105 | height: ATLAS_SIZE, | |
| 106 | depth_or_array_layers: 1, | |
| 107 | }, | |
| 108 | mip_level_count: 1, | |
| 109 | sample_count: 1, | |
| 110 | dimension: wgpu::TextureDimension::D2, | |
| 111 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 112 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 113 | view_formats: &[], | |
| 114 | }); | |
| 115 | let view = atlas.create_view(&Default::default()); | |
| 116 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 117 | mag_filter: wgpu::FilterMode::Nearest, | |
| 118 | min_filter: wgpu::FilterMode::Nearest, | |
| 119 | ..Default::default() | |
| 120 | }); | |
| 121 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 122 | label: Some("Glyph atlas"), | |
| 123 | layout: &pipeline.get_bind_group_layout(0), | |
| 124 | entries: &[ | |
| 125 | wgpu::BindGroupEntry { | |
| 126 | binding: 0, | |
| 127 | resource: wgpu::BindingResource::TextureView(&view), | |
| 128 | }, | |
| 129 | wgpu::BindGroupEntry { | |
| 130 | binding: 1, | |
| 131 | resource: wgpu::BindingResource::Sampler(&sampler), | |
| 132 | }, | |
| 133 | ], | |
| 134 | }); | |
| 101 | let (atlas, bind_group) = atlas(&device, &pipeline, ATLAS_SIZE); | |
| 135 | 102 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { |
| 136 | 103 | label: Some("Draw vertices"), |
| 137 | 104 | size: VERTEX_BUFFER_BYTES, |
| ... | ... | @@ -167,6 +134,14 @@ impl Renderer { |
| 167 | 134 | self.device.limits().max_texture_dimension_2d |
| 168 | 135 | } |
| 169 | 136 | |
| 137 | pub(super) fn atlas_side(&self) -> u32 { | |
| 138 | self.gpu.atlas.width() | |
| 139 | } | |
| 140 | ||
| 141 | pub(super) fn new_atlas(&mut self, side: u32) { | |
| 142 | (self.gpu.atlas, self.gpu.bind_group) = atlas(&self.device, &self.gpu.pipeline, side); | |
| 143 | } | |
| 144 | ||
| 170 | 145 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { |
| 171 | 146 | self.queue.write_texture( |
| 172 | 147 | wgpu::TexelCopyTextureInfo { |
| ... | ... | @@ -336,3 +311,46 @@ impl Renderer { |
| 336 | 311 | self.queue.submit([encoder.finish()]); |
| 337 | 312 | } |
| 338 | 313 | } |
| 314 | ||
| 315 | /// An empty atlas `side` texels square, and how the pipeline samples it. | |
| 316 | fn atlas( | |
| 317 | device: &wgpu::Device, | |
| 318 | pipeline: &wgpu::RenderPipeline, | |
| 319 | side: u32, | |
| 320 | ) -> (wgpu::Texture, wgpu::BindGroup) { | |
| 321 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | |
| 322 | label: Some("Glyph atlas"), | |
| 323 | size: wgpu::Extent3d { | |
| 324 | width: side, | |
| 325 | height: side, | |
| 326 | depth_or_array_layers: 1, | |
| 327 | }, | |
| 328 | mip_level_count: 1, | |
| 329 | sample_count: 1, | |
| 330 | dimension: wgpu::TextureDimension::D2, | |
| 331 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 332 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 333 | view_formats: &[], | |
| 334 | }); | |
| 335 | let view = atlas.create_view(&Default::default()); | |
| 336 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 337 | mag_filter: wgpu::FilterMode::Nearest, | |
| 338 | min_filter: wgpu::FilterMode::Nearest, | |
| 339 | ..Default::default() | |
| 340 | }); | |
| 341 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 342 | label: Some("Glyph atlas"), | |
| 343 | layout: &pipeline.get_bind_group_layout(0), | |
| 344 | entries: &[ | |
| 345 | wgpu::BindGroupEntry { | |
| 346 | binding: 0, | |
| 347 | resource: wgpu::BindingResource::TextureView(&view), | |
| 348 | }, | |
| 349 | wgpu::BindGroupEntry { | |
| 350 | binding: 1, | |
| 351 | resource: wgpu::BindingResource::Sampler(&sampler), | |
| 352 | }, | |
| 353 | ], | |
| 354 | }); | |
| 355 | (atlas, bind_group) | |
| 356 | } |
crates/mobile/Cargo.toml+1| ... | ... | @@ -16,6 +16,7 @@ onestore = { path = "../onestore" } |
| 16 | 16 | wgpu.workspace = true |
| 17 | 17 | parley.workspace = true |
| 18 | 18 | pollster = "0.4" |
| 19 | resvg = { version = "0.48.1", default-features = false } | |
| 19 | 20 | serde = { version = "1", features = ["derive"] } |
| 20 | 21 | serde_json = "1" |
| 21 | 22 |
crates/mobile/include/snowbound.h+14-1| ... | ... | @@ -14,6 +14,9 @@ void sb_string_free(char *text); |
| 14 | 14 | |
| 15 | 15 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); |
| 16 | 16 | void sb_set_coordinator(sb_coordinator coordinator); |
| 17 | void sb_set_sync_wake(void (*wake)(void)); | |
| 18 | char *sb_tags(void); | |
| 19 | bool sb_tag_icon(uint16_t shape, bool checked, uint32_t pixels, uint8_t *rgba); | |
| 17 | 20 | |
| 18 | 21 | const Library *sb_library_open(const char *path, const char *cache, char **error); |
| 19 | 22 | const Library *sb_library_server(const char *address, const char *share, const char *user, const char *password, |
| ... | ... | @@ -22,6 +25,10 @@ char *sb_library_sections(const Library *library); |
| 22 | 25 | char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author, |
| 23 | 26 | const char *date, const char *time); |
| 24 | 27 | char *sb_library_search(const Library *library, const Section *open, const char *path, const char *query); |
| 28 | char *sb_library_tagged(const Library *library, const Section *open, const char *path); | |
| 29 | char *sb_library_sync_status(const Library *library); | |
| 30 | void sb_library_set_offline(const Library *library, bool offline); | |
| 31 | void sb_library_sync_now(const Library *library); | |
| 25 | 32 | void sb_library_free(const Library *library); |
| 26 | 33 | |
| 27 | 34 | Share *sb_share_connect(const char *address, const char *share, const char *user, const char *password, |
| ... | ... | @@ -58,11 +65,17 @@ bool sb_view_drag(View *view, float x, float y); |
| 58 | 65 | bool sb_view_release(View *view); |
| 59 | 66 | bool sb_view_undo(View *view, bool redo); |
| 60 | 67 | bool sb_view_can_undo(const View *view, bool redo); |
| 61 | uint32_t sb_view_format(const View *view); | |
| 68 | uint64_t sb_view_format(const View *view); | |
| 62 | 69 | bool sb_view_apply(View *view, uint8_t command); |
| 63 | 70 | char *sb_view_title(const View *view); |
| 64 | 71 | bool sb_view_focus_title(View *view); |
| 65 | 72 | bool sb_view_find(View *view, const char *query); |
| 73 | bool sb_view_select_paragraph(View *view, const char *id); | |
| 74 | char *sb_view_copy(View *view, bool cut); | |
| 75 | void sb_view_insert_space(View *view); | |
| 76 | char *sb_view_paper(const View *view); | |
| 77 | bool sb_view_set_paper(View *view, int16_t red, uint8_t green, uint8_t blue, int8_t ruled); | |
| 78 | bool sb_view_set_art(View *view, const char *name); | |
| 66 | 79 | char *sb_view_page_text(const View *view); |
| 67 | 80 | int8_t sb_view_date_request(View *view, int64_t *seconds); |
| 68 | 81 | bool sb_view_change_date(View *view, int64_t seconds, const char *date, const char *time); |
crates/mobile/src/lib.rs+210-24| ... | ... | @@ -78,9 +78,9 @@ enum Target { |
| 78 | 78 | Grip = 3, |
| 79 | 79 | } |
| 80 | 80 | |
| 81 | /// Formatting the toolbar shows and applies, by bit in `sb_view_format` and by number in | |
| 82 | /// `sb_view_apply`: toggles, lists, indentation, then OneNote's nine default tags from 16. | |
| 83 | const FORMATS: [Formatting; 8] = [ | |
| 81 | /// Formatting the host shows and applies, by bit in `sb_view_format` and by number in | |
| 82 | /// `sb_view_apply`; OneNote's default tags follow from 16, in their list's order. | |
| 83 | const FORMATS: [Formatting; 11] = [ | |
| 84 | 84 | Formatting::Toggle(Toggle::Bold), |
| 85 | 85 | Formatting::Toggle(Toggle::Italic), |
| 86 | 86 | Formatting::Toggle(Toggle::Underline), |
| ... | ... | @@ -89,17 +89,10 @@ const FORMATS: [Formatting; 8] = [ |
| 89 | 89 | Formatting::Numbering, |
| 90 | 90 | Formatting::Indent, |
| 91 | 91 | Formatting::Outdent, |
| 92 | ]; | |
| 93 | const TAGS: [NoteTag; 9] = [ | |
| 94 | NoteTag::ToDo, | |
| 95 | NoteTag::Important, | |
| 96 | NoteTag::Question, | |
| 97 | NoteTag::RememberForLater, | |
| 98 | NoteTag::Definition, | |
| 99 | NoteTag::Highlight, | |
| 100 | NoteTag::Contact, | |
| 101 | NoteTag::Address, | |
| 102 | NoteTag::PhoneNumber, | |
| 92 | Formatting::RemoveTags, | |
| 93 | Formatting::Clear, | |
| 94 | // OneNote's first highlighter colour, yellow. | |
| 95 | Formatting::Highlight(Some(0x0000_ffff)), | |
| 103 | 96 | ]; |
| 104 | 97 | |
| 105 | 98 | /// The page and its text model, apart from the surface it is drawn on. |
| ... | ... | @@ -217,6 +210,9 @@ impl Canvas { |
| 217 | 210 | } |
| 218 | 211 | |
| 219 | 212 | fn target(&self, point: [f32; 2]) -> Target { |
| 213 | if self.page.inserting_space() { | |
| 214 | return Target::Grip; | |
| 215 | } | |
| 220 | 216 | match self.page.hit(self.device(point)) { |
| 221 | 217 | Some(Hit::Handle { .. } | Hit::Resize { .. }) => Target::Grip, |
| 222 | 218 | Some(Hit::Image { id, .. }) |
| ... | ... | @@ -363,8 +359,8 @@ impl Canvas { |
| 363 | 359 | })) |
| 364 | 360 | } |
| 365 | 361 | |
| 366 | /// The formatting the selection has, as `FORMATS` and `TAGS` bits. | |
| 367 | fn format_bits(&self) -> Result<u32> { | |
| 362 | /// The formatting the selection has, as `FORMATS` and default tag bits. | |
| 363 | fn format_bits(&self) -> Result<u64> { | |
| 368 | 364 | let state = self.page.editor.format_state()?; |
| 369 | 365 | let mut bits = 0; |
| 370 | 366 | for (bit, format) in FORMATS.iter().enumerate() { |
| ... | ... | @@ -374,17 +370,25 @@ impl Canvas { |
| 374 | 370 | Formatting::Numbering => state.numbering, |
| 375 | 371 | _ => false, |
| 376 | 372 | }; |
| 377 | bits |= u32::from(on) << bit; | |
| 373 | bits |= u64::from(on) << bit; | |
| 378 | 374 | } |
| 379 | for (bit, tag) in TAGS.iter().enumerate() { | |
| 380 | bits |= u32::from(state.tags.contains(tag)) << (16 + bit); | |
| 375 | for (place, tag) in NoteTag::defaults().into_iter().enumerate() { | |
| 376 | let tagged = state.tags.contains(&(tag, place as u16)); | |
| 377 | bits |= u64::from(tagged) << (16 + place); | |
| 381 | 378 | } |
| 382 | 379 | Ok(bits) |
| 383 | 380 | } |
| 384 | 381 | |
| 385 | 382 | fn format(&mut self, command: u8) -> Result<bool> { |
| 386 | 383 | let command = match command { |
| 387 | 16.. => Formatting::Tag(*TAGS.get(usize::from(command - 16)).ok_or("No such tag")?), | |
| 384 | 16.. => { | |
| 385 | let place = command - 16; | |
| 386 | let tag = NoteTag::defaults() | |
| 387 | .into_iter() | |
| 388 | .nth(usize::from(place)) | |
| 389 | .ok_or("No such tag")?; | |
| 390 | Formatting::Tag(tag, place.into()) | |
| 391 | } | |
| 388 | 392 | _ => FORMATS |
| 389 | 393 | .get(usize::from(command)) |
| 390 | 394 | .ok_or("No such formatting")? |
| ... | ... | @@ -426,6 +430,17 @@ impl Canvas { |
| 426 | 430 | Ok(true) |
| 427 | 431 | } |
| 428 | 432 | |
| 433 | /// Selects paragraph `id`, as a Tags Summary entry shows it. | |
| 434 | fn select_paragraph(&mut self, id: ExGuid) -> Result<bool> { | |
| 435 | let Some((outline, selection)) = canvas::search::paragraph_match(&self.page.editor, id) | |
| 436 | else { | |
| 437 | return Ok(false); | |
| 438 | }; | |
| 439 | self.page.editor.focus_outline(outline)?; | |
| 440 | self.page.editor.select(selection)?; | |
| 441 | Ok(true) | |
| 442 | } | |
| 443 | ||
| 429 | 444 | /// The page's text, outline by outline, as the desktop puts a refused edit's on the |
| 430 | 445 | /// clipboard. |
| 431 | 446 | fn page_text(&self) -> String { |
| ... | ... | @@ -446,6 +461,27 @@ impl Canvas { |
| 446 | 461 | .join("\n\n") |
| 447 | 462 | } |
| 448 | 463 | |
| 464 | /// Gives the page `color`, a COLORREF, and rule lines `ruled` of `RULE_LINES`, or none. | |
| 465 | fn set_paper(&mut self, color: Option<u32>, ruled: Option<usize>) -> Result<bool> { | |
| 466 | let rules = ruled | |
| 467 | .and_then(|index| canvas::template::RULE_LINES.get(index)) | |
| 468 | .map(|(_, lines)| *lines); | |
| 469 | Ok(moved(self.page.set_paper(color, rules, None)?)) | |
| 470 | } | |
| 471 | ||
| 472 | /// Puts template `name`'s art behind the page in place of any it had, or none. | |
| 473 | fn set_art(&mut self, name: Option<&str>) -> Result<bool> { | |
| 474 | let art = match name { | |
| 475 | Some(name) => canvas::template::find(name) | |
| 476 | .ok_or("No such template")? | |
| 477 | .pictures()?, | |
| 478 | None => Vec::new(), | |
| 479 | }; | |
| 480 | let editor = &self.page.editor; | |
| 481 | let (color, rules) = (editor.page_color(), editor.rule_lines()); | |
| 482 | Ok(moved(self.page.set_paper(color, rules, Some(art))?)) | |
| 483 | } | |
| 484 | ||
| 449 | 485 | /// Puts a picture of `size` points after the caret's paragraph. |
| 450 | 486 | fn insert_picture(&mut self, bytes: &[u8], size: [f32; 2]) -> Result<bool> { |
| 451 | 487 | let image = Image { |
| ... | ... | @@ -855,15 +891,15 @@ pub extern "C" fn sb_view_can_undo(view: &View, redo: bool) -> bool { |
| 855 | 891 | } |
| 856 | 892 | |
| 857 | 893 | /// The selection's formatting as bits: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4 |
| 858 | /// bulleted, 5 numbered, and from 16 OneNote's default tags in order, To Do first. | |
| 894 | /// bulleted, 5 numbered, and from 16 OneNote's tags in `sb_tags` order, To Do first. | |
| 859 | 895 | #[unsafe(no_mangle)] |
| 860 | pub extern "C" fn sb_view_format(view: &View) -> u32 { | |
| 896 | pub extern "C" fn sb_view_format(view: &View) -> u64 { | |
| 861 | 897 | report(view.canvas.format_bits()).unwrap_or(0) |
| 862 | 898 | } |
| 863 | 899 | |
| 864 | 900 | /// Applies formatting to the selection: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4 |
| 865 | /// bullets, 5 numbering, 6 indent, 7 outdent, each toggling where it can; from 16 toggles | |
| 866 | /// OneNote's default tag `command - 16`, To Do first. | |
| 901 | /// bullets, 5 numbering, 6 indent, 7 outdent, 8 remove tags, 9 clear formatting, 10 yellow | |
| 902 | /// highlight, each toggling where it can; from 16 toggles tag `command - 16` of `sb_tags`. | |
| 867 | 903 | #[unsafe(no_mangle)] |
| 868 | 904 | pub extern "C" fn sb_view_apply(view: &mut View, command: u8) -> bool { |
| 869 | 905 | let result = view.canvas.format(command); |
| ... | ... | @@ -893,6 +929,156 @@ pub unsafe extern "C" fn sb_view_find(view: &mut View, query: *const c_char) -> |
| 893 | 929 | report(view.canvas.find(&string(query))).unwrap_or(false) |
| 894 | 930 | } |
| 895 | 931 | |
| 932 | /// Selects paragraph `id` of the page; false where the page has none. | |
| 933 | /// | |
| 934 | /// # Safety | |
| 935 | /// `id` is NUL-terminated UTF-8. | |
| 936 | #[unsafe(no_mangle)] | |
| 937 | pub unsafe extern "C" fn sb_view_select_paragraph(view: &mut View, id: *const c_char) -> bool { | |
| 938 | let result = string(id) | |
| 939 | .parse() | |
| 940 | .map_err(Into::into) | |
| 941 | .and_then(|id| view.canvas.select_paragraph(id)); | |
| 942 | report(result).unwrap_or(false) | |
| 943 | } | |
| 944 | ||
| 945 | /// Copy, or Cut with `cut`: the selected text for the clipboard, a page selection's | |
| 946 | /// outlines with a blank line between; null with nothing selected. | |
| 947 | #[unsafe(no_mangle)] | |
| 948 | pub extern "C" fn sb_view_copy(view: &mut View, cut: bool) -> *mut c_char { | |
| 949 | let mut copied = None; | |
| 950 | let result = view.canvas.page.copy(cut).map(|response| { | |
| 951 | if let Some(Request::Copy(text)) = &response.request { | |
| 952 | copied = Some(text.clone()); | |
| 953 | } | |
| 954 | moved(response) | |
| 955 | }); | |
| 956 | view.stored(result); | |
| 957 | copied.map_or(std::ptr::null_mut(), owned) | |
| 958 | } | |
| 959 | ||
| 960 | /// Insert Space: the next drag on the page moves what lies below or right of where it | |
| 961 | /// starts, as far as it goes. | |
| 962 | #[unsafe(no_mangle)] | |
| 963 | pub extern "C" fn sb_view_insert_space(view: &mut View) { | |
| 964 | let _ = view.canvas.page.insert_space(); | |
| 965 | } | |
| 966 | ||
| 967 | #[derive(serde::Serialize)] | |
| 968 | struct Choices { | |
| 969 | /// The page colour as sRGB, or null. | |
| 970 | color: Option<[u8; 3]>, | |
| 971 | /// Index into `rules` of the page's rule lines, or null for none or others. | |
| 972 | ruled: Option<usize>, | |
| 973 | /// OneNote's page colours, rule lines and templates, by name; colours with their sRGB. | |
| 974 | colors: Vec<(&'static str, [u8; 3])>, | |
| 975 | rules: Vec<&'static str>, | |
| 976 | templates: Vec<&'static str>, | |
| 977 | } | |
| 978 | ||
| 979 | fn srgb(colorref: u32) -> [u8; 3] { | |
| 980 | let [red, green, blue, _] = colorref.to_le_bytes(); | |
| 981 | [red, green, blue] | |
| 982 | } | |
| 983 | ||
| 984 | /// The page's paper and what it can take, as JSON: `color`, `ruled`, and the choices | |
| 985 | /// `colors` (each `[name, [r, g, b]]`), `rules` and `templates`, by name. | |
| 986 | #[unsafe(no_mangle)] | |
| 987 | pub extern "C" fn sb_view_paper(view: &View) -> *mut c_char { | |
| 988 | use canvas::template::{PAGE_COLORS, RULE_LINES, TEMPLATES}; | |
| 989 | let editor = &view.canvas.page.editor; | |
| 990 | let rules = editor.rule_lines(); | |
| 991 | library::json(Ok(Choices { | |
| 992 | color: editor.page_color().map(srgb), | |
| 993 | ruled: RULE_LINES | |
| 994 | .iter() | |
| 995 | .position(|(_, lines)| Some(*lines) == rules), | |
| 996 | colors: PAGE_COLORS | |
| 997 | .iter() | |
| 998 | .map(|(name, color)| (*name, srgb(*color))) | |
| 999 | .collect(), | |
| 1000 | rules: RULE_LINES.iter().map(|(name, _)| *name).collect(), | |
| 1001 | templates: TEMPLATES.iter().map(|template| template.name).collect(), | |
| 1002 | })) | |
| 1003 | } | |
| 1004 | ||
| 1005 | /// Gives the page the colour `red`, `green`, `blue`, or none when `red` is negative, and | |
| 1006 | /// rule lines `ruled` of `sb_view_paper`'s `rules`, or none when negative, as one undo step. | |
| 1007 | #[unsafe(no_mangle)] | |
| 1008 | pub extern "C" fn sb_view_set_paper( | |
| 1009 | view: &mut View, | |
| 1010 | red: i16, | |
| 1011 | green: u8, | |
| 1012 | blue: u8, | |
| 1013 | ruled: i8, | |
| 1014 | ) -> bool { | |
| 1015 | let color = u8::try_from(red) | |
| 1016 | .ok() | |
| 1017 | .map(|red| u32::from_le_bytes([red, green, blue, 0])); | |
| 1018 | let result = view.canvas.set_paper(color, usize::try_from(ruled).ok()); | |
| 1019 | view.stored(result) | |
| 1020 | } | |
| 1021 | ||
| 1022 | /// Puts template `name`'s art behind the page in place of any it had, or none when `name` | |
| 1023 | /// is null, as one undo step; its colour and rule lines stay. | |
| 1024 | /// | |
| 1025 | /// # Safety | |
| 1026 | /// A non-null `name` is NUL-terminated UTF-8. | |
| 1027 | #[unsafe(no_mangle)] | |
| 1028 | pub unsafe extern "C" fn sb_view_set_art(view: &mut View, name: *const c_char) -> bool { | |
| 1029 | let result = view.canvas.set_art(library::optional(name).as_deref()); | |
| 1030 | view.stored(result) | |
| 1031 | } | |
| 1032 | ||
| 1033 | /// OneNote's default tags as JSON, in `sb_view_apply`'s order from 16: each `[name, shape, | |
| 1034 | /// highlight]`, the shape being the symbol `sb_tag_icon` draws and the highlight sRGB or | |
| 1035 | /// null. | |
| 1036 | #[unsafe(no_mangle)] | |
| 1037 | pub extern "C" fn sb_tags() -> *mut c_char { | |
| 1038 | library::json(Ok(NoteTag::defaults() | |
| 1039 | .into_iter() | |
| 1040 | .map(|tag| (tag.label, tag.shape, tag.highlight.map(srgb))) | |
| 1041 | .collect::<Vec<_>>())) | |
| 1042 | } | |
| 1043 | ||
| 1044 | /// Draws tag symbol `shape`, a check box `checked`, into `rgba`, `pixels` square of | |
| 1045 | /// premultiplied RGBA; false for shape 0, a highlighting tag, which has none. | |
| 1046 | /// | |
| 1047 | /// # Safety | |
| 1048 | /// `rgba` has room for `pixels * pixels * 4` bytes. | |
| 1049 | #[unsafe(no_mangle)] | |
| 1050 | pub unsafe extern "C" fn sb_tag_icon( | |
| 1051 | shape: u16, | |
| 1052 | checked: bool, | |
| 1053 | pixels: u32, | |
| 1054 | rgba: *mut u8, | |
| 1055 | ) -> bool { | |
| 1056 | use resvg::{tiny_skia, usvg}; | |
| 1057 | let Some(icon) = canvas::outline::TagIcon::of(shape, checked) else { | |
| 1058 | return false; | |
| 1059 | }; | |
| 1060 | let Some(mut pixmap) = tiny_skia::Pixmap::new(pixels, pixels) else { | |
| 1061 | return false; | |
| 1062 | }; | |
| 1063 | for source in canvas::gpu::tag_sources(icon) { | |
| 1064 | let Some(tree) = | |
| 1065 | report(usvg::Tree::from_str(source, &usvg::Options::default()).map_err(Into::into)) | |
| 1066 | else { | |
| 1067 | return false; | |
| 1068 | }; | |
| 1069 | let size = tree.size(); | |
| 1070 | let scale = pixels as f32 / size.width().max(size.height()); | |
| 1071 | resvg::render( | |
| 1072 | &tree, | |
| 1073 | tiny_skia::Transform::from_scale(scale, scale), | |
| 1074 | &mut pixmap.as_mut(), | |
| 1075 | ); | |
| 1076 | } | |
| 1077 | // SAFETY: the caller's contract. | |
| 1078 | unsafe { std::ptr::copy_nonoverlapping(pixmap.data().as_ptr(), rgba, pixmap.data().len()) }; | |
| 1079 | true | |
| 1080 | } | |
| 1081 | ||
| 896 | 1082 | /// The page's text, for the clipboard when an edit is refused; freed with `sb_string_free`. |
| 897 | 1083 | #[unsafe(no_mangle)] |
| 898 | 1084 | pub extern "C" fn sb_view_page_text(view: &View) -> *mut c_char { |
crates/mobile/src/library.rs+286-35| ... | ... | @@ -3,11 +3,11 @@ |
| 3 | 3 | //! cache, which stores each edit at once and publishes it in the background. |
| 4 | 4 | |
| 5 | 5 | use crate::{Result, owned, report, string}; |
| 6 | use canvas::search::{Entry, Index, Query}; | |
| 6 | use canvas::search::{Entry, Index, Query, Tagged}; | |
| 7 | 7 | use notebook::{ |
| 8 | 8 | Remote, Replica, |
| 9 | 9 | discover::{Folder, SectionState}, |
| 10 | session::{self, Event, Notebook}, | |
| 10 | session::{self, Background, Event, Notebook, SyncStatus}, | |
| 11 | 11 | smb::{Client, Credentials}, |
| 12 | 12 | }; |
| 13 | 13 | use onestore::{ |
| ... | ... | @@ -21,8 +21,8 @@ use std::{ |
| 21 | 21 | io, |
| 22 | 22 | path::{Path, PathBuf}, |
| 23 | 23 | sync::{ |
| 24 | Arc, Mutex, OnceLock, | |
| 25 | atomic::{AtomicU8, Ordering}, | |
| 24 | Arc, Mutex, OnceLock, Weak, | |
| 25 | atomic::{AtomicBool, AtomicU8, Ordering}, | |
| 26 | 26 | }, |
| 27 | 27 | time::{Duration, Instant}, |
| 28 | 28 | }; |
| ... | ... | @@ -33,6 +33,24 @@ const TIMEOUT: Duration = Duration::from_secs(10); |
| 33 | 33 | const LIMIT: usize = 256 * 1024 * 1024; |
| 34 | 34 | /// The tab colour OneNote gives a section that stores none, as the desktop shows it. |
| 35 | 35 | const SECTION_COLOR: u32 = 0x00e4_a88a; |
| 36 | /// How often sections no page shows are polled, as the desktop polls them. | |
| 37 | const BACKGROUND: Duration = Duration::from_secs(15); | |
| 38 | ||
| 39 | /// Called on a background thread when a notebook's sync status may have changed. | |
| 40 | static SYNC_WAKE: OnceLock<extern "C" fn()> = OnceLock::new(); | |
| 41 | ||
| 42 | /// Has the host call `wake` whenever a notebook's closed sections report; set once, before | |
| 43 | /// any library opens. | |
| 44 | #[unsafe(no_mangle)] | |
| 45 | pub extern "C" fn sb_set_sync_wake(wake: extern "C" fn()) { | |
| 46 | let _ = SYNC_WAKE.set(wake); | |
| 47 | } | |
| 48 | ||
| 49 | fn sync_woken() { | |
| 50 | if let Some(wake) = SYNC_WAKE.get() { | |
| 51 | wake(); | |
| 52 | } | |
| 53 | } | |
| 36 | 54 | |
| 37 | 55 | /// Runs `body(context)` within the host's coordinated reading or writing of `path`, as |
| 38 | 56 | /// `NSFileCoordinator` has other processes (file providers) wait for it, or not at all when |
| ... | ... | @@ -183,6 +201,12 @@ pub struct Library { |
| 183 | 201 | place: Place, |
| 184 | 202 | cache: PathBuf, |
| 185 | 203 | index: Mutex<(HashMap<String, Checked>, Index)>, |
| 204 | /// Syncs the sections no session holds; none for a lone section. | |
| 205 | background: Option<Background>, | |
| 206 | /// The sections open for editing, by catalog path, whose sessions sync them. | |
| 207 | open: Mutex<Vec<(String, Weak<Shared>)>>, | |
| 208 | /// Work Offline, which sections opened later follow too. | |
| 209 | offline: AtomicBool, | |
| 186 | 210 | } |
| 187 | 211 | |
| 188 | 212 | #[derive(serde::Serialize, serde::Deserialize)] |
| ... | ... | @@ -244,12 +268,20 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) { |
| 244 | 268 | |
| 245 | 269 | impl Library { |
| 246 | 270 | pub(crate) fn open(path: &Path, cache: &Path) -> Result<Self> { |
| 247 | let (notebook, place) = if path.is_file() { | |
| 248 | (None, Place::File(path.to_owned())) | |
| 271 | let (notebook, place, background) = if path.is_file() { | |
| 272 | (None, Place::File(path.to_owned()), None) | |
| 249 | 273 | } else { |
| 274 | let notebook = Notebook::open(path, cache)?; | |
| 275 | let background = notebook.background_with( | |
| 276 | BACKGROUND, | |
| 277 | |file| Coordinated(file.to_owned()), | |
| 278 | sync_woken, | |
| 279 | )?; | |
| 280 | background.watch(notebook.replicas()); | |
| 250 | 281 | ( |
| 251 | Some(Notebook::open(path, cache)?), | |
| 282 | Some(notebook), | |
| 252 | 283 | Place::Folder(path.to_owned()), |
| 284 | Some(background), | |
| 253 | 285 | ) |
| 254 | 286 | }; |
| 255 | 287 | Ok(Self { |
| ... | ... | @@ -257,30 +289,61 @@ impl Library { |
| 257 | 289 | place, |
| 258 | 290 | cache: cache.to_owned(), |
| 259 | 291 | index: Mutex::default(), |
| 292 | background, | |
| 293 | open: Mutex::default(), | |
| 294 | offline: AtomicBool::new(false), | |
| 260 | 295 | }) |
| 261 | 296 | } |
| 262 | 297 | |
| 263 | 298 | /// The notebook folder `root` on `server`; while the server cannot be reached, its sections |
| 264 | 299 | /// as last listed, if it was listed before. |
| 265 | 300 | pub(crate) fn server(server: Server, root: &str, cache: &Path) -> Result<Self> { |
| 301 | let server = Arc::new(server); | |
| 302 | let connect = Arc::clone(&server); | |
| 303 | let background = Background::smb( | |
| 304 | root, | |
| 305 | LIMIT, | |
| 306 | BACKGROUND, | |
| 307 | move || connect.connect(), | |
| 308 | sync_woken, | |
| 309 | )?; | |
| 266 | 310 | let library = Self { |
| 267 | 311 | notebook: Mutex::default(), |
| 268 | 312 | place: Place::Share { |
| 269 | server: Arc::new(server), | |
| 313 | server, | |
| 270 | 314 | client: Mutex::default(), |
| 271 | 315 | root: root.to_owned(), |
| 272 | 316 | }, |
| 273 | 317 | cache: cache.to_owned(), |
| 274 | 318 | index: Mutex::default(), |
| 319 | background: Some(background), | |
| 320 | open: Mutex::default(), | |
| 321 | offline: AtomicBool::new(false), | |
| 275 | 322 | }; |
| 276 | if let Err(error) = library.with_notebook(false, |_| Ok(())) | |
| 277 | && library.listed().is_none() | |
| 278 | { | |
| 279 | return Err(error); | |
| 323 | let reached = library.with_notebook(false, |_| Ok(())); | |
| 324 | match library.listed() { | |
| 325 | Some(listed) => library.watch(&listed.files), | |
| 326 | None => reached?, | |
| 280 | 327 | } |
| 281 | 328 | Ok(library) |
| 282 | 329 | } |
| 283 | 330 | |
| 331 | /// Has the background sync a share notebook's sections, `files` as `Listed` keeps them. | |
| 332 | fn watch(&self, files: &BTreeMap<String, String>) { | |
| 333 | let Some(background) = &self.background else { | |
| 334 | return; | |
| 335 | }; | |
| 336 | let smb = self.cache.join("smb"); | |
| 337 | background.watch( | |
| 338 | files | |
| 339 | .iter() | |
| 340 | .map(|(path, identity)| { | |
| 341 | (path.clone(), Some(smb.join(format!("{identity}.sqlite")))) | |
| 342 | }) | |
| 343 | .collect(), | |
| 344 | ); | |
| 345 | } | |
| 346 | ||
| 284 | 347 | /// Where a share notebook's last listing is kept. |
| 285 | 348 | fn listing(&self) -> Option<PathBuf> { |
| 286 | 349 | let Place::Share { server, root, .. } = &self.place else { |
| ... | ... | @@ -366,6 +429,9 @@ impl Library { |
| 366 | 429 | }; |
| 367 | 430 | tabs(notebook.catalog(), &mut listed.tabs); |
| 368 | 431 | files(notebook.catalog(), &mut listed.files); |
| 432 | if let (Place::Folder(_), Some(background)) = (&self.place, &self.background) { | |
| 433 | background.watch(notebook.replicas()); | |
| 434 | } | |
| 369 | 435 | Ok(listed) |
| 370 | 436 | }); |
| 371 | 437 | let Some(listing) = self.listing() else { |
| ... | ... | @@ -375,14 +441,42 @@ impl Library { |
| 375 | 441 | Ok(listed) => { |
| 376 | 442 | std::fs::create_dir_all(listing.parent().ok_or("No cache")?)?; |
| 377 | 443 | std::fs::write(&listing, serde_json::to_vec(&listed)?)?; |
| 444 | self.watch(&listed.files); | |
| 378 | 445 | Ok(listed.tabs) |
| 379 | 446 | } |
| 380 | 447 | Err(error) => Ok(self.listed().ok_or(error)?.tabs), |
| 381 | 448 | } |
| 382 | 449 | } |
| 383 | 450 | |
| 384 | /// The section at catalog `path`, through a replica in the cache. | |
| 451 | /// The section at catalog `path`, through a replica in the cache, following Work | |
| 452 | /// Offline. The background may hold the replica for a step, which takes a network round | |
| 453 | /// trip, so a busy replica is tried again for a while. | |
| 385 | 454 | pub(crate) fn section( |
| 455 | &self, | |
| 456 | path: &str, | |
| 457 | notify: impl Fn() + Send + Sync + 'static, | |
| 458 | ) -> Result<session::Section> { | |
| 459 | let deadline = Instant::now() + TIMEOUT * 3; | |
| 460 | let notify = Arc::new(notify); | |
| 461 | let section = loop { | |
| 462 | let notify = Arc::clone(&notify); | |
| 463 | match self.section_once(path, move || notify()) { | |
| 464 | Err(error) | |
| 465 | if Instant::now() < deadline | |
| 466 | && error | |
| 467 | .downcast_ref::<notebook::Error>() | |
| 468 | .is_some_and(notebook::Error::busy) => | |
| 469 | { | |
| 470 | std::thread::sleep(Duration::from_millis(50)); | |
| 471 | } | |
| 472 | opened => break opened?, | |
| 473 | } | |
| 474 | }; | |
| 475 | section.set_offline(self.offline.load(Ordering::Relaxed)); | |
| 476 | Ok(section) | |
| 477 | } | |
| 478 | ||
| 479 | fn section_once( | |
| 386 | 480 | &self, |
| 387 | 481 | path: &str, |
| 388 | 482 | notify: impl Fn() + Send + 'static, |
| ... | ... | @@ -518,6 +612,27 @@ fn dated(author: &str, date: &str, time: &str) -> Result<PageCreation> { |
| 518 | 612 | } |
| 519 | 613 | |
| 520 | 614 | impl Section { |
| 615 | /// The section at catalog `path` of `library`, its edits naming `author`. | |
| 616 | pub(crate) fn open( | |
| 617 | library: Arc<Library>, | |
| 618 | path: &str, | |
| 619 | author: String, | |
| 620 | notify: impl Fn() + Send + Sync + 'static, | |
| 621 | ) -> Result<Self> { | |
| 622 | let shared = Arc::new(Shared { | |
| 623 | section: library.section(path, notify)?, | |
| 624 | author, | |
| 625 | status: AtomicU8::new(0), | |
| 626 | }); | |
| 627 | let mut open = library | |
| 628 | .open | |
| 629 | .lock() | |
| 630 | .unwrap_or_else(|error| error.into_inner()); | |
| 631 | open.push((path.to_owned(), Arc::downgrade(&shared))); | |
| 632 | drop(open); | |
| 633 | Ok(Self { shared, library }) | |
| 634 | } | |
| 635 | ||
| 521 | 636 | pub(crate) fn rows(&self) -> Result<Vec<Row>> { |
| 522 | 637 | let section = &self.shared.section; |
| 523 | 638 | let conflicts = section.conflicts()?; |
| ... | ... | @@ -684,9 +799,50 @@ pub(crate) struct Found { |
| 684 | 799 | |
| 685 | 800 | impl Library { |
| 686 | 801 | /// Pages of the notebook holding every word of `query` in their title or text, as the |
| 687 | /// desktop searches: the open section as its edits leave it, others as stored, read | |
| 688 | /// again when their file changed. | |
| 802 | /// desktop searches. | |
| 689 | 803 | pub(crate) fn search(&self, open: Option<(&str, &Section)>, query: &str) -> Result<Vec<Found>> { |
| 804 | self.indexed(open, |index| { | |
| 805 | index | |
| 806 | .search(&Query::new(query), |_| true) | |
| 807 | .into_iter() | |
| 808 | .map(|found| Found { | |
| 809 | section: found.section, | |
| 810 | page: found.space.to_string(), | |
| 811 | title: found.title.replace(|char: char| char.is_control(), " "), | |
| 812 | snippet: found.snippet, | |
| 813 | }) | |
| 814 | .collect() | |
| 815 | }) | |
| 816 | } | |
| 817 | ||
| 818 | /// The notebook's tagged paragraphs in page order, as OneNote's Tags Summary lists | |
| 819 | /// them: once for each of their tags. | |
| 820 | pub(crate) fn tagged(&self, open: Option<(&str, &Section)>) -> Result<Vec<Tag>> { | |
| 821 | self.indexed(open, |index| { | |
| 822 | index | |
| 823 | .tagged(|_| true) | |
| 824 | .into_iter() | |
| 825 | .map(|tagged: Tagged| Tag { | |
| 826 | section: tagged.section, | |
| 827 | page: tagged.space.to_string(), | |
| 828 | title: tagged.title.replace(|char: char| char.is_control(), " "), | |
| 829 | paragraph: tagged.paragraph.to_string(), | |
| 830 | name: tagged.name, | |
| 831 | shape: tagged.shape, | |
| 832 | checked: tagged.checked, | |
| 833 | text: tagged.text, | |
| 834 | }) | |
| 835 | .collect() | |
| 836 | }) | |
| 837 | } | |
| 838 | ||
| 839 | /// `read` over the notebook's pages: the open section as its edits leave it, others as | |
| 840 | /// stored, read again when their file changed. | |
| 841 | fn indexed<T>( | |
| 842 | &self, | |
| 843 | open: Option<(&str, &Section)>, | |
| 844 | read: impl FnOnce(&Index) -> T, | |
| 845 | ) -> Result<T> { | |
| 690 | 846 | let tabs: Vec<String> = self |
| 691 | 847 | .tabs()? |
| 692 | 848 | .into_iter() |
| ... | ... | @@ -714,18 +870,73 @@ impl Library { |
| 714 | 870 | } |
| 715 | 871 | } |
| 716 | 872 | index.retain(|entry| tabs.contains(&entry.section)); |
| 717 | Ok(index | |
| 718 | .search(&Query::new(query), |_| true) | |
| 873 | Ok(read(index)) | |
| 874 | } | |
| 875 | ||
| 876 | /// Each section's sync status in the notebook's order: an open section's from its | |
| 877 | /// session, the others' from the background. | |
| 878 | pub(crate) fn sync_status(&self) -> Vec<Synced> { | |
| 879 | let mut sections = self | |
| 880 | .background | |
| 881 | .as_ref() | |
| 882 | .map(Background::status) | |
| 883 | .unwrap_or_default(); | |
| 884 | for (path, shared) in self.open_sections() { | |
| 885 | let Some(status) = report(shared.section.sync_status().map_err(Into::into)) else { | |
| 886 | continue; | |
| 887 | }; | |
| 888 | match sections.iter_mut().find(|(listed, _)| *listed == path) { | |
| 889 | Some((_, listed)) => *listed = status, | |
| 890 | None => sections.push((path, status)), | |
| 891 | } | |
| 892 | } | |
| 893 | sections | |
| 719 | 894 | .into_iter() |
| 720 | .map(|found| Found { | |
| 721 | section: found.section, | |
| 722 | page: found.space.to_string(), | |
| 723 | title: found.title.replace(|char: char| char.is_control(), " "), | |
| 724 | snippet: found.snippet, | |
| 895 | .map(|(path, status): (String, SyncStatus)| Synced { | |
| 896 | state: status.state() as u8, | |
| 897 | synced: status.synced.map(unix), | |
| 898 | queued: status.queued, | |
| 899 | error: status.error.map(|error| error.to_string()), | |
| 900 | path, | |
| 725 | 901 | }) |
| 726 | .collect()) | |
| 902 | .collect() | |
| 903 | } | |
| 904 | ||
| 905 | fn open_sections(&self) -> Vec<(String, Arc<Shared>)> { | |
| 906 | let mut open = self.open.lock().unwrap_or_else(|error| error.into_inner()); | |
| 907 | open.retain(|(_, shared)| shared.strong_count() > 0); | |
| 908 | open.iter() | |
| 909 | .filter_map(|(path, shared)| Some((path.clone(), shared.upgrade()?))) | |
| 910 | .collect() | |
| 727 | 911 | } |
| 912 | } | |
| 728 | 913 | |
| 914 | #[derive(serde::Serialize)] | |
| 915 | pub(crate) struct Synced { | |
| 916 | pub(crate) path: String, | |
| 917 | /// `SyncState` by its place in the enum, best first. | |
| 918 | pub(crate) state: u8, | |
| 919 | /// When the section file was last reached, in seconds since 1970. | |
| 920 | pub(crate) synced: Option<i64>, | |
| 921 | /// Changes the file does not hold yet. | |
| 922 | pub(crate) queued: u64, | |
| 923 | pub(crate) error: Option<String>, | |
| 924 | } | |
| 925 | ||
| 926 | /// A tagged paragraph, from `sb_library_tagged`. | |
| 927 | #[derive(serde::Serialize)] | |
| 928 | pub(crate) struct Tag { | |
| 929 | pub(crate) section: String, | |
| 930 | pub(crate) page: String, | |
| 931 | pub(crate) title: String, | |
| 932 | pub(crate) paragraph: String, | |
| 933 | pub(crate) name: String, | |
| 934 | pub(crate) shape: u16, | |
| 935 | pub(crate) checked: bool, | |
| 936 | pub(crate) text: String, | |
| 937 | } | |
| 938 | ||
| 939 | impl Library { | |
| 729 | 940 | /// Reads the section at catalog `path` into `index` again when its file changed, and |
| 730 | 941 | /// checks at most every 30 seconds. |
| 731 | 942 | fn reindex( |
| ... | ... | @@ -814,12 +1025,12 @@ fn boxed<T>(result: Result<T>, error: *mut *mut c_char) -> *mut T { |
| 814 | 1025 | } |
| 815 | 1026 | } |
| 816 | 1027 | |
| 817 | fn json(value: Result<impl serde::Serialize>) -> *mut c_char { | |
| 1028 | pub(crate) fn json(value: Result<impl serde::Serialize>) -> *mut c_char { | |
| 818 | 1029 | report(value.and_then(|value| Ok(serde_json::to_string(&value)?))) |
| 819 | 1030 | .map_or(std::ptr::null_mut(), owned) |
| 820 | 1031 | } |
| 821 | 1032 | |
| 822 | fn optional(text: *const c_char) -> Option<String> { | |
| 1033 | pub(crate) fn optional(text: *const c_char) -> Option<String> { | |
| 823 | 1034 | (!text.is_null()).then(|| string(text)) |
| 824 | 1035 | } |
| 825 | 1036 | |
| ... | ... | @@ -925,6 +1136,55 @@ pub unsafe extern "C" fn sb_library_search( |
| 925 | 1136 | json(library.search(open.map(|section| (path.as_str(), section)), &string(query))) |
| 926 | 1137 | } |
| 927 | 1138 | |
| 1139 | /// The notebook's tagged paragraphs in page order as JSON, once for each of their tags: | |
| 1140 | /// each with `section`, `page`, `title`, `paragraph`, `name`, `shape` (the tag's symbol, 0 | |
| 1141 | /// for a highlighting tag), `checked` and `text`. `open` and `path` as `sb_library_search` | |
| 1142 | /// takes them; reads the notebook's sections, so call it off the main thread. | |
| 1143 | /// | |
| 1144 | /// # Safety | |
| 1145 | /// A non-null `path` is NUL-terminated UTF-8. | |
| 1146 | #[unsafe(no_mangle)] | |
| 1147 | pub unsafe extern "C" fn sb_library_tagged( | |
| 1148 | library: &Library, | |
| 1149 | open: Option<&Section>, | |
| 1150 | path: *const c_char, | |
| 1151 | ) -> *mut c_char { | |
| 1152 | let path = optional(path).unwrap_or_default(); | |
| 1153 | json(library.tagged(open.map(|section| (path.as_str(), section)))) | |
| 1154 | } | |
| 1155 | ||
| 1156 | /// Each section's sync status as JSON, in the notebook's order: `path`, `state` (0 up to | |
| 1157 | /// date, 1 syncing, 2 in use elsewhere, 3 not connected, 4 read-only, 5 failed), `synced` | |
| 1158 | /// (seconds since 1970, or null before the file was reached), `queued` changes and `error`. | |
| 1159 | #[unsafe(no_mangle)] | |
| 1160 | pub extern "C" fn sb_library_sync_status(library: &Library) -> *mut c_char { | |
| 1161 | json(Ok(library.sync_status())) | |
| 1162 | } | |
| 1163 | ||
| 1164 | /// Works offline, or online again: edits wait in the cache until then or until | |
| 1165 | /// `sb_library_sync_now`. Sections opened later follow. | |
| 1166 | #[unsafe(no_mangle)] | |
| 1167 | pub extern "C" fn sb_library_set_offline(library: &Library, offline: bool) { | |
| 1168 | library.offline.store(offline, Ordering::Relaxed); | |
| 1169 | if let Some(background) = &library.background { | |
| 1170 | background.set_offline(offline); | |
| 1171 | } | |
| 1172 | for (_, shared) in library.open_sections() { | |
| 1173 | shared.section.set_offline(offline); | |
| 1174 | } | |
| 1175 | } | |
| 1176 | ||
| 1177 | /// Syncs every section of the notebook now, working offline included. | |
| 1178 | #[unsafe(no_mangle)] | |
| 1179 | pub extern "C" fn sb_library_sync_now(library: &Library) { | |
| 1180 | if let Some(background) = &library.background { | |
| 1181 | background.wake(); | |
| 1182 | } | |
| 1183 | for (_, shared) in library.open_sections() { | |
| 1184 | shared.section.wake(); | |
| 1185 | } | |
| 1186 | } | |
| 1187 | ||
| 928 | 1188 | /// # Safety |
| 929 | 1189 | /// `library` came from `sb_library_open` or `sb_library_server` and is not used again. |
| 930 | 1190 | #[unsafe(no_mangle)] |
| ... | ... | @@ -1050,16 +1310,7 @@ pub unsafe extern "C" fn sb_section_open( |
| 1050 | 1310 | Arc::from_raw(library as *const Library) |
| 1051 | 1311 | }; |
| 1052 | 1312 | boxed( |
| 1053 | library | |
| 1054 | .section(&string(path), move || wake(token)) | |
| 1055 | .map(|section| Section { | |
| 1056 | shared: Arc::new(Shared { | |
| 1057 | section, | |
| 1058 | author: string(author), | |
| 1059 | status: AtomicU8::new(0), | |
| 1060 | }), | |
| 1061 | library, | |
| 1062 | }), | |
| 1313 | Section::open(library, &string(path), string(author), move || wake(token)), | |
| 1063 | 1314 | error, |
| 1064 | 1315 | ) |
| 1065 | 1316 | } |
crates/mobile/src/tests.rs+206-10| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | 1 | use super::*; |
| 2 | use library::{sb_library_set_offline, sb_library_sync_now}; | |
| 3 | use notebook::session::SyncState; | |
| 2 | 4 | use std::path::{Path, PathBuf}; |
| 3 | 5 | |
| 4 | 6 | const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus"); |
| ... | ... | @@ -24,15 +26,7 @@ fn copy(path: &str) -> (tempfile::TempDir, PathBuf) { |
| 24 | 26 | } |
| 25 | 27 | |
| 26 | 28 | fn open(library: &Arc<Library>, path: &str) -> Section { |
| 27 | let section = library.section(path, || {}).unwrap(); | |
| 28 | Section { | |
| 29 | shared: Arc::new(Shared { | |
| 30 | section, | |
| 31 | author: "Clover Test".into(), | |
| 32 | status: Default::default(), | |
| 33 | }), | |
| 34 | library: Arc::clone(library), | |
| 35 | } | |
| 29 | Section::open(Arc::clone(library), path, "Clover Test".into(), || {}).unwrap() | |
| 36 | 30 | } |
| 37 | 31 | |
| 38 | 32 | fn space(section: &Section, title: &str) -> ExGuid { |
| ... | ... | @@ -230,7 +224,20 @@ fn undo_restores_typing() { |
| 230 | 224 | let mut canvas = canvas(&section, "Paragraph controls"); |
| 231 | 225 | focus(&mut canvas, "Collapsed parent"); |
| 232 | 226 | let before = canvas.active().shown_text(); |
| 233 | canvas.insert("hi".into()).unwrap(); | |
| 227 | // UIKit's keystrokes, one call each, with the selection set again where it is: one run. | |
| 228 | for text in ["h", "i", "x"] { | |
| 229 | canvas.insert(text.into()).unwrap(); | |
| 230 | let selection = canvas.selection().unwrap(); | |
| 231 | canvas.select(selection).unwrap(); | |
| 232 | } | |
| 233 | assert!( | |
| 234 | canvas | |
| 235 | .page | |
| 236 | .key(&Key::Named(NamedKey::Backspace), None) | |
| 237 | .unwrap() | |
| 238 | .changed | |
| 239 | ); | |
| 240 | canvas.insert("!".into()).unwrap(); | |
| 234 | 241 | assert_ne!(canvas.active().shown_text(), before); |
| 235 | 242 | assert!(moved(canvas.page.undo(false).unwrap())); |
| 236 | 243 | assert_eq!(canvas.active().shown_text(), before); |
| ... | ... | @@ -629,3 +636,192 @@ fn tapping_the_date_asks_for_it_and_a_new_date_is_stored() { |
| 629 | 636 | assert_eq!(stored.unwrap()[0], "Friday, January 2, 2026"); |
| 630 | 637 | assert_ne!(canvas.page.editor.date().unwrap().timestamp(), before); |
| 631 | 638 | } |
| 639 | ||
| 640 | /// A notebook copy with its library, and a section of it open for editing. | |
| 641 | fn notebook_open(path: &str) -> (tempfile::TempDir, PathBuf, Arc<Library>, Section) { | |
| 642 | let (directory, root) = copy("m6/native-features-01/notebook"); | |
| 643 | let library = Arc::new(Library::open(&root, &directory.path().join("cache")).unwrap()); | |
| 644 | let section = open(&library, path); | |
| 645 | (directory, root, library, section) | |
| 646 | } | |
| 647 | ||
| 648 | /// Waits up to ten seconds for `ready`. | |
| 649 | fn eventually(mut ready: impl FnMut() -> bool) -> bool { | |
| 650 | let deadline = Instant::now() + Duration::from_secs(10); | |
| 651 | while Instant::now() < deadline { | |
| 652 | if ready() { | |
| 653 | return true; | |
| 654 | } | |
| 655 | std::thread::sleep(Duration::from_millis(50)); | |
| 656 | } | |
| 657 | false | |
| 658 | } | |
| 659 | ||
| 660 | #[test] | |
| 661 | fn sync_status_lists_every_section_and_work_offline_holds_edits_until_sync_now() { | |
| 662 | let (_directory, root, library, section) = notebook_open("Features.one"); | |
| 663 | // Every readable section, the open one's from its session. | |
| 664 | assert!(eventually(|| { | |
| 665 | let status = library.sync_status(); | |
| 666 | status.len() | |
| 667 | == library | |
| 668 | .tabs() | |
| 669 | .unwrap() | |
| 670 | .iter() | |
| 671 | .filter(|tab| tab.readable) | |
| 672 | .count() | |
| 673 | && status | |
| 674 | .iter() | |
| 675 | .all(|sync| sync.state == SyncState::UpToDate as u8) | |
| 676 | })); | |
| 677 | sb_library_set_offline(&library, true); | |
| 678 | let mut canvas = canvas(&section, "Paragraph controls"); | |
| 679 | focus(&mut canvas, "Collapsed parent"); | |
| 680 | canvas.insert("Offline ".into()).unwrap(); | |
| 681 | section | |
| 682 | .shared | |
| 683 | .apply(canvas.edit().unwrap().unwrap()) | |
| 684 | .unwrap(); | |
| 685 | let file = root.join("Features.one"); | |
| 686 | let queued = |library: &Library| { | |
| 687 | library | |
| 688 | .sync_status() | |
| 689 | .into_iter() | |
| 690 | .find(|sync| sync.path == "Features.one") | |
| 691 | .unwrap() | |
| 692 | .queued | |
| 693 | }; | |
| 694 | assert!(eventually(|| queued(&library) == 1)); | |
| 695 | std::thread::sleep(Duration::from_millis(500)); | |
| 696 | assert!(!stored_text(&file, canvas.space).contains("Offline ")); | |
| 697 | sb_library_sync_now(&library); | |
| 698 | assert!(eventually(|| queued(&library) == 0)); | |
| 699 | assert!(stored_text(&file, canvas.space).contains("Offline ")); | |
| 700 | sb_library_set_offline(&library, false); | |
| 701 | } | |
| 702 | ||
| 703 | #[test] | |
| 704 | fn sections_open_the_first_time_while_the_background_syncs() { | |
| 705 | let (directory, root) = copy("m6/native-features-01/notebook"); | |
| 706 | let library = Arc::new(Library::open(&root, &directory.path().join("cache")).unwrap()); | |
| 707 | let rounds = { | |
| 708 | let library = Arc::clone(&library); | |
| 709 | std::thread::spawn(move || { | |
| 710 | for _ in 0..200 { | |
| 711 | sb_library_sync_now(&library); | |
| 712 | std::thread::sleep(Duration::from_millis(2)); | |
| 713 | } | |
| 714 | }) | |
| 715 | }; | |
| 716 | for tab in library | |
| 717 | .tabs() | |
| 718 | .unwrap() | |
| 719 | .into_iter() | |
| 720 | .filter(|tab| tab.readable) | |
| 721 | { | |
| 722 | open(&library, &tab.path); | |
| 723 | } | |
| 724 | rounds.join().unwrap(); | |
| 725 | for tab in library | |
| 726 | .tabs() | |
| 727 | .unwrap() | |
| 728 | .into_iter() | |
| 729 | .filter(|tab| tab.readable) | |
| 730 | { | |
| 731 | open(&library, &tab.path); | |
| 732 | } | |
| 733 | } | |
| 734 | ||
| 735 | #[test] | |
| 736 | fn the_tags_summary_lists_tagged_paragraphs_and_opens_on_them() { | |
| 737 | let (directory, file) = copy("structural-probe/tag-gallery.one"); | |
| 738 | let library = Arc::new(Library::open(&file, &directory.path().join("cache")).unwrap()); | |
| 739 | let path = file.to_string_lossy().into_owned(); | |
| 740 | let section = open(&library, &path); | |
| 741 | let tagged = library.tagged(Some((&path, &section))).unwrap(); | |
| 742 | assert!(tagged.len() >= 9, "{}", tagged.len()); | |
| 743 | assert!( | |
| 744 | tagged | |
| 745 | .iter() | |
| 746 | .any(|tag| tag.name == "To Do" && tag.shape == 3) | |
| 747 | ); | |
| 748 | let first = &tagged[0]; | |
| 749 | let space = first.page.parse().unwrap(); | |
| 750 | let (page, _) = section.shared.page(space).unwrap(); | |
| 751 | let mut canvas = Canvas::new(space, page, [1206, 2622], 3.0).unwrap(); | |
| 752 | assert!( | |
| 753 | canvas | |
| 754 | .select_paragraph(first.paragraph.parse().unwrap()) | |
| 755 | .unwrap() | |
| 756 | ); | |
| 757 | let [start, end] = canvas.selection().unwrap(); | |
| 758 | assert!(canvas.text([start, end]).contains(first.text.trim())); | |
| 759 | } | |
| 760 | ||
| 761 | #[test] | |
| 762 | fn every_tag_applies_and_reports_and_remove_tag_clears_them() { | |
| 763 | let (_directory, section) = features(); | |
| 764 | let mut canvas = canvas(&section, "Paragraph controls"); | |
| 765 | focus(&mut canvas, "Collapsed parent"); | |
| 766 | for index in [0, 12, 28] { | |
| 767 | assert!(canvas.format(16 + index).unwrap()); | |
| 768 | assert_ne!(canvas.format_bits().unwrap() & 1 << (16 + index), 0); | |
| 769 | } | |
| 770 | assert!(canvas.format(8).unwrap()); | |
| 771 | assert_eq!(canvas.format_bits().unwrap() >> 16, 0); | |
| 772 | assert!(canvas.format(16 + NoteTag::defaults().len() as u8).is_err()); | |
| 773 | } | |
| 774 | ||
| 775 | #[test] | |
| 776 | fn tag_icons_draw_their_art_and_highlighting_tags_have_none() { | |
| 777 | let mut rgba = vec![0; 32 * 32 * 4]; | |
| 778 | for tag in NoteTag::defaults() { | |
| 779 | let drawn = unsafe { sb_tag_icon(tag.shape, true, 32, rgba.as_mut_ptr()) }; | |
| 780 | assert_eq!(drawn, tag.shape != 0, "{tag:?}"); | |
| 781 | if drawn { | |
| 782 | assert!(rgba.chunks_exact(4).any(|pixel| pixel[3] > 0), "{tag:?}"); | |
| 783 | } | |
| 784 | } | |
| 785 | } | |
| 786 | ||
| 787 | #[test] | |
| 788 | fn page_colour_rule_lines_and_art_are_stored_and_undo() { | |
| 789 | let (_directory, section) = features(); | |
| 790 | let mut canvas = canvas(&section, "Paragraph controls"); | |
| 791 | assert!(canvas.set_paper(Some(0xfef5ed), Some(1)).unwrap()); | |
| 792 | assert!(canvas.set_art(Some("Bamboo")).unwrap()); | |
| 793 | section | |
| 794 | .shared | |
| 795 | .apply(canvas.edit().unwrap().unwrap()) | |
| 796 | .unwrap(); | |
| 797 | let art = |page: &Page| { | |
| 798 | page.objects | |
| 799 | .iter() | |
| 800 | .filter(|object| { | |
| 801 | matches!(object, onestore::page::PageObject::Image(image) if image.background) | |
| 802 | }) | |
| 803 | .count() | |
| 804 | }; | |
| 805 | let stored = section.shared.page(canvas.space).unwrap().0; | |
| 806 | assert_eq!(stored.color, Some(0xfef5ed)); | |
| 807 | assert_eq!(stored.rule_lines, Some(canvas::template::RULE_LINES[1].1)); | |
| 808 | assert_eq!(art(&stored), 1); | |
| 809 | assert!(canvas.set_art(None).unwrap()); | |
| 810 | assert!(moved(canvas.page.undo(false).unwrap())); | |
| 811 | assert!(moved(canvas.page.undo(false).unwrap())); | |
| 812 | section | |
| 813 | .shared | |
| 814 | .apply(canvas.edit().unwrap().unwrap()) | |
| 815 | .unwrap(); | |
| 816 | let stored = section.shared.page(canvas.space).unwrap().0; | |
| 817 | assert_eq!((stored.color, art(&stored)), (Some(0xfef5ed), 0)); | |
| 818 | } | |
| 819 | ||
| 820 | #[test] | |
| 821 | fn insert_space_turns_the_next_touch_into_a_drag() { | |
| 822 | let (_directory, section) = features(); | |
| 823 | let mut canvas = canvas(&section, "Paragraph controls"); | |
| 824 | assert_eq!(canvas.target([300.0, 1500.0]), Target::Page); | |
| 825 | let _ = canvas.page.insert_space(); | |
| 826 | assert_eq!(canvas.target([300.0, 1500.0]), Target::Grip); | |
| 827 | } |
crates/notebook/src/lib.rs+23-4| ... | ... | @@ -14,7 +14,7 @@ use rusqlite::{Connection, OpenFlags, TransactionBehavior, params}; |
| 14 | 14 | use std::{ |
| 15 | 15 | fs::OpenOptions, |
| 16 | 16 | io, |
| 17 | path::Path, | |
| 17 | path::{Path, PathBuf}, | |
| 18 | 18 | sync::{Arc, Mutex, MutexGuard, mpsc}, |
| 19 | 19 | time::Duration, |
| 20 | 20 | }; |
| ... | ... | @@ -106,6 +106,25 @@ impl Replica { |
| 106 | 106 | /// An initialization error preserves the created file for inspection. |
| 107 | 107 | pub fn create(path: impl AsRef<Path>, source: &[u8]) -> Result<Self> { |
| 108 | 108 | validate(source)?; |
| 109 | let path = path.as_ref(); | |
| 110 | // Built under another name and linked into place whole, so that a cache that exists | |
| 111 | // is complete: a background step or a failed creation never leaves one half made. | |
| 112 | let mut building = path.as_os_str().to_owned(); | |
| 113 | building.push(".creating"); | |
| 114 | let building = PathBuf::from(building); | |
| 115 | for stale in ["", "-wal", "-shm"] { | |
| 116 | let mut file = building.as_os_str().to_owned(); | |
| 117 | file.push(stale); | |
| 118 | let _ = std::fs::remove_file(file); | |
| 119 | } | |
| 120 | let built = Self::build(&building, source); | |
| 121 | let linked = built.and_then(|()| Ok(std::fs::hard_link(&building, path)?)); | |
| 122 | let _ = std::fs::remove_file(&building); | |
| 123 | linked?; | |
| 124 | Self::start(cache_connection(path)?) | |
| 125 | } | |
| 126 | ||
| 127 | fn build(path: &Path, source: &[u8]) -> Result<()> { | |
| 109 | 128 | let mut options = OpenOptions::new(); |
| 110 | 129 | options.read(true).write(true).create_new(true); |
| 111 | 130 | #[cfg(unix)] |
| ... | ... | @@ -113,8 +132,8 @@ impl Replica { |
| 113 | 132 | use std::os::unix::fs::OpenOptionsExt; |
| 114 | 133 | options.mode(0o600); |
| 115 | 134 | } |
| 116 | drop(options.open(path.as_ref())?); | |
| 117 | let mut connection = cache_connection(path.as_ref())?; | |
| 135 | drop(options.open(path)?); | |
| 136 | let mut connection = cache_connection(path)?; | |
| 118 | 137 | write_ahead(&connection)?; |
| 119 | 138 | let transaction = connection.transaction_with_behavior(TransactionBehavior::Exclusive)?; |
| 120 | 139 | let tables: i64 = |
| ... | ... | @@ -133,7 +152,7 @@ impl Replica { |
| 133 | 152 | schema::create(&transaction)?; |
| 134 | 153 | base::write(&transaction, base::Image::Base, source)?; |
| 135 | 154 | transaction.commit()?; |
| 136 | Self::start(connection) | |
| 155 | Ok(connection.close().map_err(|(_, error)| error)?) | |
| 137 | 156 | } |
| 138 | 157 | |
| 139 | 158 | /// Reopens an existing cache and its durable pending edits without network access. |
crates/notebook/src/merge.rs+6-1| ... | ... | @@ -696,7 +696,9 @@ impl Diff { |
| 696 | 696 | let part = |index: &Index| { |
| 697 | 697 | index.nodes.get(object).map(|node| match (&node.own, edit) { |
| 698 | 698 | ( |
| 699 | Own::Outline { layout, .. } | Own::Ink(Ink { layout, .. }), | |
| 699 | Own::Outline { layout, .. } | |
| 700 | | Own::Ink(Ink { layout, .. }) | |
| 701 | | Own::Attachment(Attachment { layout, .. }), | |
| 700 | 702 | OutlineEdit::Position { .. }, |
| 701 | 703 | ) => (layout.x, layout.y, None, None), |
| 702 | 704 | (Own::Outline { layout, .. }, OutlineEdit::Width { .. }) => ( |
| ... | ... | @@ -1073,6 +1075,9 @@ impl Index { |
| 1073 | 1075 | PageObject::Image(image) => { |
| 1074 | 1076 | index.add(image.id, PAGE, Own::Image(image.clone()), Vec::new()) |
| 1075 | 1077 | } |
| 1078 | PageObject::Attachment(file) => { | |
| 1079 | index.add(file.id, PAGE, Own::Attachment(file.clone()), Vec::new()) | |
| 1080 | } | |
| 1076 | 1081 | PageObject::Ink(ink) => index.add(ink.id, PAGE, Own::Ink(ink.clone()), Vec::new()), |
| 1077 | 1082 | PageObject::Unsupported(object) => index.add( |
| 1078 | 1083 | object.id, |
crates/notebook/src/queue.rs+4| ... | ... | @@ -47,6 +47,10 @@ fn object(object: &mut PageObject, visit: &mut impl FnMut(&mut Payload)) { |
| 47 | 47 | } |
| 48 | 48 | } |
| 49 | 49 | PageObject::Image(image) => visit(&mut image.bytes), |
| 50 | PageObject::Attachment(attachment) => { | |
| 51 | visit(&mut attachment.bytes); | |
| 52 | visit(&mut attachment.preview); | |
| 53 | } | |
| 50 | 54 | PageObject::Ink(_) | PageObject::Unsupported(_) => {} |
| 51 | 55 | } |
| 52 | 56 | } |
crates/notebook/src/session.rs+46-2| ... | ... | @@ -919,6 +919,17 @@ impl Notebook { |
| 919 | 919 | &self, |
| 920 | 920 | interval: Duration, |
| 921 | 921 | notify: impl Fn() + Send + 'static, |
| 922 | ) -> Result<Background> { | |
| 923 | self.background_with(interval, |file| FileRemote(file.to_owned()), notify) | |
| 924 | } | |
| 925 | ||
| 926 | /// `background`, reaching each section file through the remote `remote` makes for it | |
| 927 | /// (`section_with`). | |
| 928 | pub fn background_with<R: Remote + 'static>( | |
| 929 | &self, | |
| 930 | interval: Duration, | |
| 931 | remote: impl Fn(&Path) -> R + Clone + Send + 'static, | |
| 932 | notify: impl Fn() + Send + 'static, | |
| 922 | 933 | ) -> Result<Background> { |
| 923 | 934 | let Some(root) = self.root.clone() else { |
| 924 | 935 | return Err(io::Error::new( |
| ... | ... | @@ -930,8 +941,8 @@ impl Notebook { |
| 930 | 941 | Background::start( |
| 931 | 942 | interval, |
| 932 | 943 | move || { |
| 933 | let root = root.clone(); | |
| 934 | Ok(move |path: &str| FileRemote(root.join(path))) | |
| 944 | let (root, remote) = (root.clone(), remote.clone()); | |
| 945 | Ok(move |path: &str| remote(&root.join(path))) | |
| 935 | 946 | }, |
| 936 | 947 | notify, |
| 937 | 948 | ) |
| ... | ... | @@ -1110,6 +1121,39 @@ pub struct SyncStatus { |
| 1110 | 1121 | pub queued: u64, |
| 1111 | 1122 | } |
| 1112 | 1123 | |
| 1124 | /// What a `SyncStatus` comes to for the reader, from the best to the worst. | |
| 1125 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] | |
| 1126 | pub enum SyncState { | |
| 1127 | UpToDate, | |
| 1128 | /// Not reached yet, or edits wait. | |
| 1129 | Syncing, | |
| 1130 | /// Another client holds the section file. | |
| 1131 | InUse, | |
| 1132 | /// The file or its server cannot be reached; edits wait until it can. | |
| 1133 | NotConnected, | |
| 1134 | /// The file cannot be written where it is stored. | |
| 1135 | ReadOnly, | |
| 1136 | Failed, | |
| 1137 | } | |
| 1138 | ||
| 1139 | impl SyncStatus { | |
| 1140 | pub fn state(&self) -> SyncState { | |
| 1141 | use io::ErrorKind::*; | |
| 1142 | match self.error.as_ref().map(io::Error::kind) { | |
| 1143 | Some(PermissionDenied | ReadOnlyFilesystem) => SyncState::ReadOnly, | |
| 1144 | Some(WouldBlock | ResourceBusy) => SyncState::InUse, | |
| 1145 | Some( | |
| 1146 | NotFound | ConnectionRefused | ConnectionReset | ConnectionAborted | NotConnected | |
| 1147 | | TimedOut | HostUnreachable | NetworkUnreachable | NetworkDown | BrokenPipe | |
| 1148 | | AddrNotAvailable, | |
| 1149 | ) => SyncState::NotConnected, | |
| 1150 | Some(_) => SyncState::Failed, | |
| 1151 | None if self.synced.is_none() || self.queued > 0 => SyncState::Syncing, | |
| 1152 | None => SyncState::UpToDate, | |
| 1153 | } | |
| 1154 | } | |
| 1155 | } | |
| 1156 | ||
| 1113 | 1157 | /// The last attempt's outcome; `None` before the first. |
| 1114 | 1158 | type Observed = Option<Option<io::Error>>; |
| 1115 | 1159 |
crates/notebook/tests/copy.rs+4-3| ... | ... | @@ -11,11 +11,12 @@ use std::{ |
| 11 | 11 | time::{Duration, Instant}, |
| 12 | 12 | }; |
| 13 | 13 | |
| 14 | /// Pages the writer authored for other rows: text with an attachment and its icon, a | |
| 15 | /// table, nested tables with cell subtrees, an inserted picture, a page-level ink drawing, tags, lists, equations | |
| 16 | /// and paragraph formatting. | |
| 14 | /// Pages the writer authored for other rows: text with an attachment and its icon, files on | |
| 15 | /// the page, a table, nested tables with cell subtrees, an inserted picture, a page-level ink | |
| 16 | /// drawing, tags, lists, equations and paragraph formatting. | |
| 17 | 17 | const SOURCES: &[&str] = &[ |
| 18 | 18 | "attachment-edit/icon/candidate/files.one", |
| 19 | "attachment-floating/candidate/files.one", | |
| 19 | 20 | "table-edit/created/candidate/tables.one", |
| 20 | 21 | "table-edit/nested/candidate/synthetic.one", |
| 21 | 22 | "picture-edit/inserted/candidate/pictures.one", |
crates/notebook/tests/sync_outline.rs+90| ... | ... | @@ -614,3 +614,93 @@ fn a_new_native_wrap_reservation_keeps_the_local_width_on_a_conflict_page() { |
| 614 | 614 | assert_eq!(layout.max_width, Some(144.0)); |
| 615 | 615 | assert!(layout.reserved_width.is_none()); |
| 616 | 616 | } |
| 617 | ||
| 618 | /// `source` with a file on its page, and the file's identity. | |
| 619 | fn with_file(source: &[u8], space: ExGuid) -> (Vec<u8>, ExGuid) { | |
| 620 | let id = onestore::page::text::new_id().unwrap(); | |
| 621 | let file = onestore::page::Attachment { | |
| 622 | id, | |
| 623 | filename: "float.txt".into(), | |
| 624 | source_path: None, | |
| 625 | size: Some([24.0, 24.0]), | |
| 626 | layout: onestore::document::Layout { | |
| 627 | x: Some(342.0), | |
| 628 | y: Some(284.4), | |
| 629 | max_width: Some(54.0), | |
| 630 | max_height: Some(63.0), | |
| 631 | width_set_by_user: Some(false), | |
| 632 | reserved_width: None, | |
| 633 | }, | |
| 634 | bytes: Some(std::sync::Arc::from(&b"Floating bytes"[..])), | |
| 635 | preview: None, | |
| 636 | recording: None, | |
| 637 | }; | |
| 638 | let image = remote_with(source, space, |page| { | |
| 639 | page.objects.push(PageObject::Attachment(file)) | |
| 640 | }); | |
| 641 | (image, id) | |
| 642 | } | |
| 643 | ||
| 644 | fn file_mut(page: &mut Page, id: ExGuid) -> &mut onestore::page::Attachment { | |
| 645 | page.objects | |
| 646 | .iter_mut() | |
| 647 | .find_map(|object| match object { | |
| 648 | PageObject::Attachment(file) if file.id == id => Some(file), | |
| 649 | _ => None, | |
| 650 | }) | |
| 651 | .unwrap() | |
| 652 | } | |
| 653 | ||
| 654 | /// A file on the page merges as an outline's position does: a local move and a remote rename | |
| 655 | /// both land, and competing moves keep the local one on a conflict page. | |
| 656 | #[test] | |
| 657 | fn files_on_the_page_move_through_sync_and_competing_moves_conflict() { | |
| 658 | let (source, space, _, text_id) = fixture(); | |
| 659 | let (source, file) = with_file(&source, space); | |
| 660 | let place = |x, y| { | |
| 661 | move |page: &mut Page| { | |
| 662 | let layout = &mut file_mut(page, file).layout; | |
| 663 | [layout.x, layout.y] = [Some(x), Some(y)]; | |
| 664 | } | |
| 665 | }; | |
| 666 | let directory = tempfile::tempdir().unwrap(); | |
| 667 | let cache = Replica::create(directory.path().join("cache.sqlite"), &source).unwrap(); | |
| 668 | let id = save(&cache, text_id, place(126.0, 410.4)).unwrap().unwrap(); | |
| 669 | let mut server = Server::new(&remote_with(&source, space, |page| { | |
| 670 | file_mut(page, file).filename = "renamed.txt".into(); | |
| 671 | })); | |
| 672 | published(&cache, &mut server, id); | |
| 673 | let mut durable = page_of(&server.durable, space); | |
| 674 | let merged = file_mut(&mut durable, file); | |
| 675 | assert_eq!( | |
| 676 | (merged.layout.x, merged.layout.y, merged.filename.as_str()), | |
| 677 | (Some(126.0), Some(410.4), "renamed.txt") | |
| 678 | ); | |
| 679 | assert_eq!(merged.bytes.as_deref(), Some(&b"Floating bytes"[..])); | |
| 680 | ||
| 681 | let directory = tempfile::tempdir().unwrap(); | |
| 682 | let cache = Replica::create(directory.path().join("cache.sqlite"), &source).unwrap(); | |
| 683 | save(&cache, text_id, place(126.0, 410.4)).unwrap().unwrap(); | |
| 684 | let remote = remote_with(&source, space, place(36.0, 50.4)); | |
| 685 | let mut server = Server::new(&remote); | |
| 686 | assert!(conflicted(&cache, &mut server, space)); | |
| 687 | let mut durable = page_of(&server.durable, space); | |
| 688 | assert_eq!(file_mut(&mut durable, file).layout.x, Some(36.0)); | |
| 689 | let arena = onestore::Arena::default(); | |
| 690 | let mut section = onestore::Section::open(&arena, server.durable.clone()).unwrap(); | |
| 691 | let conflict = section.conflicts().unwrap()[0].1[0].space; | |
| 692 | let kept = section.page(conflict).unwrap(); | |
| 693 | let copies: Vec<_> = kept | |
| 694 | .objects | |
| 695 | .iter() | |
| 696 | .filter_map(|object| match object { | |
| 697 | PageObject::Attachment(file) => Some(file), | |
| 698 | _ => None, | |
| 699 | }) | |
| 700 | .collect(); | |
| 701 | let [copy] = copies.as_slice() else { | |
| 702 | panic!("{kept:?}") | |
| 703 | }; | |
| 704 | assert_eq!((copy.layout.x, copy.layout.y), (Some(126.0), Some(410.4))); | |
| 705 | assert_eq!(copy.bytes.as_deref(), Some(&b"Floating bytes"[..])); | |
| 706 | } |
crates/onestore/README.md+2-2| ... | ... | @@ -46,7 +46,7 @@ harness also accepts `--client-profile release`. |
| 46 | 46 | | `Page::copy` | The page's content under fresh identities for copying into another section, definitions and payloads included; indent levels only an outline group carries refuse | |
| 47 | 47 | | `page::link::internal_link`, `page::link::parse_internal_link` | Build and read the `onenote:#…` URLs OneNote stores for links to sections, pages and paragraphs, by identity | |
| 48 | 48 | | `page::Recording`, `page::MediaIndex` | A recording attachment's identity and type, and a paragraph's link to a moment in recordings; read from native pages and kept through edits, never authored | |
| 49 | | `page::Page`, `page::Paragraph`, `page::Ink`, `page::Math` | Build an editable page model (title, outlines, paragraphs with coalesced text spans, tables, images, attachments, ink drawings and handwriting decoded to stroke polylines in page points, a moved drawing offset by its position) with stored identities; equations parse from their linear text and run data into a tree that renders the MathML OneNote exports; content outside the model is retained as `Unsupported` | | |
| 49 | | `page::Page`, `page::Paragraph`, `page::Ink`, `page::Math` | Build an editable page model (title, outlines, paragraphs with coalesced text spans, tables, images, attachments in paragraphs or on the page, ink drawings and handwriting decoded to stroke polylines in page points, a moved drawing offset by its position) with stored identities; equations parse from their linear text and run data into a tree that renders the MathML OneNote exports; content outside the model is retained as `Unsupported` | | |
| 50 | 50 | | `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned | |
| 51 | 51 | | `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode | |
| 52 | 52 | | `PageCreation`, `SectionOp::Create` | Add an empty top-level page and its section entry atomically, under identities the intent retains across retries | |
| ... | ... | @@ -56,7 +56,7 @@ harness also accepts `--client-profile release`. |
| 56 | 56 | | `TocEdit`, `edit_table_of_contents` | Add, rename, order and remove a table of contents' section and group entries, and colour the notebook, as one revision's `Transaction`; a section's colour is `SectionOp::Color` in its own metadata | |
| 57 | 57 | | `place`, `place_file` | Name a file for its notebook as OneNote does on adoption (parent TOC identity and name CRC in the header), so OneNote keeps its identity, on any `CommitIo` or under the filesystem adapter | |
| 58 | 58 | | `TextAttribute`, `PageOp::Format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | |
| 59 | | `OutlineEdit`, `PageOp::Outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | | |
| 59 | | `OutlineEdit`, `PageOp::Outline` | Change ordinary outline position/width, the position of a file or ink drawing on the page, or a paragraph's saved expansion default, preserving identities and content | | |
| 60 | 60 | | `Transaction`, `Stamp` | The bytes a commit writes (appended data, in-place list-tail and log patches, header) and the header and length it requires unchanged; `commit` under caller-held exclusion, `commit_file` under the conservative filesystem adapter, `apply` to the base image in memory; serializable for queues | |
| 61 | 61 | | `Arena`, `Section` | Keep a section parsed across edits: `open` validates an image once; `seal` appends one revision per changed space as a `Transaction` on `stamp`, checking only what it appends; `replay` applies a queued transaction; `image` and `page` read the result | |
| 62 | 62 | | `op::{Edit, Op, PageOp, TableEdit, SectionOp}`, `Section::apply` | Object-level edits (text, formatting, links, equations, paragraph insertion/split/join/move/deletion/levels, outlines, lists, tags, styles, paragraph formatting, tables, pictures, attachments, ink, page creation/import/moves/removal, conflict pages) applied whole or not to a kept-open section with emitter-chosen identities, UTF-16 ranges and the edit's time; refusals name the target, identity or structure at fault | |
crates/onestore/src/op/apply.rs+34-24| ... | ... | @@ -1090,25 +1090,7 @@ impl<'a> Writer<'_, 'a> { |
| 1090 | 1090 | self.picture(image, Some(paragraph.id)) |
| 1091 | 1091 | } |
| 1092 | 1092 | ParagraphContent::Attachment(attachment) => { |
| 1093 | let Some(bytes) = &attachment.bytes else { | |
| 1094 | return Err(OpError::Unsupported("A new attachment needs its payload").into()); | |
| 1095 | }; | |
| 1096 | let ids = AttachmentIds { | |
| 1097 | object: attachment.id, | |
| 1098 | file: fresh()?, | |
| 1099 | payload: crate::write::fresh_guid()?, | |
| 1100 | preview: match &attachment.preview { | |
| 1101 | Some(_) => Some((crate::write::fresh_guid()?, fresh()?)), | |
| 1102 | None => None, | |
| 1103 | }, | |
| 1104 | }; | |
| 1105 | let mut payloads: Vec<([u8; 16], &[u8])> = vec![(ids.payload, bytes)]; | |
| 1106 | if let (Some((payload, _)), Some(icon)) = (ids.preview, &attachment.preview) { | |
| 1107 | payloads.push((payload, icon)); | |
| 1108 | } | |
| 1109 | self.write_with(&payloads, |page| { | |
| 1110 | content::attachment_changes(page, attachment, &ids, paragraph.id) | |
| 1111 | }) | |
| 1093 | self.attachment(attachment, Some(paragraph.id)) | |
| 1112 | 1094 | } |
| 1113 | 1095 | ParagraphContent::Ink(ink) => self.ink(ink, Some(paragraph.id)), |
| 1114 | 1096 | ParagraphContent::Unsupported(_) => { |
| ... | ... | @@ -1136,6 +1118,32 @@ impl<'a> Writer<'_, 'a> { |
| 1136 | 1118 | }) |
| 1137 | 1119 | } |
| 1138 | 1120 | |
| 1121 | fn attachment( | |
| 1122 | &mut self, | |
| 1123 | attachment: &Attachment, | |
| 1124 | holder: Option<ExGuid>, | |
| 1125 | ) -> Result<(), Failure> { | |
| 1126 | let Some(bytes) = &attachment.bytes else { | |
| 1127 | return Err(OpError::Unsupported("A new attachment needs its payload").into()); | |
| 1128 | }; | |
| 1129 | let ids = AttachmentIds { | |
| 1130 | object: attachment.id, | |
| 1131 | file: fresh()?, | |
| 1132 | payload: crate::write::fresh_guid()?, | |
| 1133 | preview: match &attachment.preview { | |
| 1134 | Some(_) => Some((crate::write::fresh_guid()?, fresh()?)), | |
| 1135 | None => None, | |
| 1136 | }, | |
| 1137 | }; | |
| 1138 | let mut payloads: Vec<([u8; 16], &[u8])> = vec![(ids.payload, bytes)]; | |
| 1139 | if let (Some((payload, _)), Some(icon)) = (ids.preview, &attachment.preview) { | |
| 1140 | payloads.push((payload, icon)); | |
| 1141 | } | |
| 1142 | self.write_with(&payloads, |page| { | |
| 1143 | content::attachment_changes(page, attachment, &ids, holder) | |
| 1144 | }) | |
| 1145 | } | |
| 1146 | ||
| 1139 | 1147 | fn ink(&mut self, ink: &Ink, holder: Option<ExGuid>) -> Result<(), Failure> { |
| 1140 | 1148 | for stroke in &ink.strokes { |
| 1141 | 1149 | self.free(stroke.id)?; |
| ... | ... | @@ -1166,13 +1174,14 @@ impl<'a> Writer<'_, 'a> { |
| 1166 | 1174 | self.free(id)?; |
| 1167 | 1175 | } |
| 1168 | 1176 | } |
| 1169 | if first.parent.is_some() || first.level != 1 { | |
| 1170 | return Err(OpError::Unsupported("An outline starts at its first level").into()); | |
| 1177 | if first.parent.is_some() { | |
| 1178 | return Err(OpError::Unsupported("An outline's first paragraph has no parent").into()); | |
| 1171 | 1179 | } |
| 1172 | 1180 | self.paragraph(outline.id, None, first, Some((outline.id, x, y)))?; |
| 1173 | // Later paragraphs follow the first, their levels as the outline gives them. | |
| 1174 | if !rest.is_empty() { | |
| 1175 | let mut targets = vec![(first.id, 1)]; | |
| 1181 | // Later paragraphs follow the first, their levels as the outline gives them; a | |
| 1182 | // deeper first paragraph goes in a group, as OneNote stores a tabbed one. | |
| 1183 | if !rest.is_empty() || first.level != 1 { | |
| 1184 | let mut targets = vec![(first.id, first.level)]; | |
| 1176 | 1185 | for paragraph in rest { |
| 1177 | 1186 | let parent = paragraph.parent.unwrap_or(outline.id); |
| 1178 | 1187 | let parent_level = rest |
| ... | ... | @@ -1226,6 +1235,7 @@ impl<'a> Writer<'_, 'a> { |
| 1226 | 1235 | } |
| 1227 | 1236 | self.picture(image, None)?; |
| 1228 | 1237 | } |
| 1238 | PageObject::Attachment(attachment) => self.attachment(attachment, None)?, | |
| 1229 | 1239 | PageObject::Ink(ink) => self.ink(ink, None)?, |
| 1230 | 1240 | PageObject::Title(_) | PageObject::Unsupported(_) => { |
| 1231 | 1241 | return Err( |
crates/onestore/src/op/content.rs+23-6| ... | ... | @@ -218,7 +218,9 @@ pub(crate) fn picture_changes( |
| 218 | 218 | [0xff, 0xd8, 0xff, ..] => ".jpg", |
| 219 | 219 | [b'G', b'I', b'F', b'8', ..] => ".gif", |
| 220 | 220 | [b'B', b'M', ..] => ".bmp", |
| 221 | _ => return Err(invalid("Choose a PNG, JPEG, GIF or BMP picture")), | |
| 221 | // OneNote keeps an imported TIFF as it is (`corpus/m6/native-features-01`). | |
| 222 | [b'I', b'I', 0x2a, 0x00, ..] | [b'M', b'M', 0x00, 0x2a, ..] => ".tif", | |
| 223 | _ => return Err(invalid("Choose a PNG, JPEG, GIF, BMP or TIFF picture")), | |
| 222 | 224 | }; |
| 223 | 225 | let modified = crate::create::current_timestamps()?.0.to_le_bytes(); |
| 224 | 226 | let mut values: Values = vec![(0x14001d7a, modified.to_vec())]; |
| ... | ... | @@ -303,12 +305,13 @@ pub(crate) struct AttachmentIds { |
| 303 | 305 | |
| 304 | 306 | /// What OneNote stores for an inserted file: an embedded-file container declaring the |
| 305 | 307 | /// payload and an attachment object naming the file, which paragraph `holder` holds as |
| 306 | /// content. The caller embeds the payloads. | |
| 308 | /// content or the page lists as a child at the attachment's position | |
| 309 | /// (`corpus/attachment-floating`). The caller embeds the payloads. | |
| 307 | 310 | pub(crate) fn attachment_changes( |
| 308 | 311 | active: &ActivePage<'_>, |
| 309 | 312 | attachment: &Attachment, |
| 310 | 313 | ids: &AttachmentIds, |
| 311 | holder: ExGuid, | |
| 314 | holder: Option<ExGuid>, | |
| 312 | 315 | ) -> Result<Changes, Error> { |
| 313 | 316 | let name = attachment.filename.as_str(); |
| 314 | 317 | if name.is_empty() || name.contains(['\0', '/', '\\']) { |
| ... | ... | @@ -351,13 +354,27 @@ pub(crate) fn attachment_changes( |
| 351 | 354 | .flat_map(|v| v.to_le_bytes()) |
| 352 | 355 | .collect(), |
| 353 | 356 | )); |
| 354 | values.push((0x14001c3e, 1u32.to_le_bytes().to_vec())); | |
| 355 | values.push((0x14001c84, 1u32.to_le_bytes().to_vec())); | |
| 357 | let positioned = attachment.layout.x.is_some() || attachment.layout.y.is_some(); | |
| 358 | if holder.is_some() == positioned { | |
| 359 | return Err(invalid( | |
| 360 | "A file on the page has a position and a paragraph's file has none", | |
| 361 | )); | |
| 362 | } | |
| 363 | // LayoutAlignmentInParent and LayoutAlignmentSelf: a paragraph's file is left-aligned in | |
| 364 | // its paragraph; one on the page is aligned by its left margin (9), as OneNote stores. | |
| 365 | match holder { | |
| 366 | Some(_) => { | |
| 367 | values.push((0x14001c3e, 1u32.to_le_bytes().to_vec())); | |
| 368 | values.push((0x14001c84, 1u32.to_le_bytes().to_vec())); | |
| 369 | } | |
| 370 | None => values.push((0x14001c84, 9u32.to_le_bytes().to_vec())), | |
| 371 | } | |
| 356 | 372 | values.push((0x1c001c22, crate::create::string(name))); |
| 357 | 373 | values.push((0x1c001d9c, crate::create::string(name))); |
| 358 | 374 | if let Some(path) = &attachment.source_path { |
| 359 | 375 | values.push((0x1c001d9d, crate::create::string(path))); |
| 360 | 376 | } |
| 377 | values.extend(layout_values(&Default::default(), &attachment.layout)?.0); | |
| 361 | 378 | let mut changed = BTreeMap::new(); |
| 362 | 379 | let mut file = PropertyObject::file(ids.file, &payload_reference(ids.payload), extension)?; |
| 363 | 380 | file.jcid = crate::write::EMBEDDED_FILE_JCID; |
| ... | ... | @@ -379,7 +396,7 @@ pub(crate) fn attachment_changes( |
| 379 | 396 | node.set(&[(0x20001c3f, &reference)])?; |
| 380 | 397 | } |
| 381 | 398 | changed.insert(ids.object, node); |
| 382 | hold(active, &mut changed, ids.object, Some(holder), &modified)?; | |
| 399 | hold(active, &mut changed, ids.object, holder, &modified)?; | |
| 383 | 400 | Ok(changed) |
| 384 | 401 | } |
| 385 | 402 |
crates/onestore/src/op/lower.rs+79-9| ... | ... | @@ -103,7 +103,10 @@ impl<'a> View<'a> { |
| 103 | 103 | view.outline(outline)?; |
| 104 | 104 | } |
| 105 | 105 | } |
| 106 | PageObject::Image(_) | PageObject::Ink(_) | PageObject::Unsupported(_) => {} | |
| 106 | PageObject::Image(_) | |
| 107 | | PageObject::Attachment(_) | |
| 108 | | PageObject::Ink(_) | |
| 109 | | PageObject::Unsupported(_) => {} | |
| 107 | 110 | } |
| 108 | 111 | } |
| 109 | 112 | Ok(view) |
| ... | ... | @@ -191,9 +194,10 @@ impl<'a> View<'a> { |
| 191 | 194 | let outlines: Vec<&Outline> = match object { |
| 192 | 195 | PageObject::Outline(outline) => vec![outline], |
| 193 | 196 | PageObject::Title(title) => title.outlines.iter().collect(), |
| 194 | PageObject::Image(_) | PageObject::Ink(_) | PageObject::Unsupported(_) => { | |
| 195 | Vec::new() | |
| 196 | } | |
| 197 | PageObject::Image(_) | |
| 198 | | PageObject::Attachment(_) | |
| 199 | | PageObject::Ink(_) | |
| 200 | | PageObject::Unsupported(_) => Vec::new(), | |
| 197 | 201 | }; |
| 198 | 202 | for outline in outlines { |
| 199 | 203 | containers.push(outline.id); |
| ... | ... | @@ -262,7 +266,10 @@ pub(crate) fn validate( |
| 262 | 266 | title.layout, |
| 263 | 267 | title.outlines.iter().map(|o| o.id).collect::<Vec<_>>() |
| 264 | 268 | )), |
| 265 | PageObject::Outline(_) | PageObject::Image(_) | PageObject::Ink(_) => None, | |
| 269 | PageObject::Outline(_) | |
| 270 | | PageObject::Image(_) | |
| 271 | | PageObject::Attachment(_) | |
| 272 | | PageObject::Ink(_) => None, | |
| 266 | 273 | }) |
| 267 | 274 | .collect(); |
| 268 | 275 | fixed.sort(); |
| ... | ... | @@ -667,7 +674,18 @@ pub(crate) fn paragraph_fields( |
| 667 | 674 | space_before: field!(space_before), |
| 668 | 675 | space_after: field!(space_after), |
| 669 | 676 | line_spacing: field!(line_spacing), |
| 670 | language: field!(language), | |
| 677 | // An unset language is the writer's default, not LCID 0, which OneNote never stores | |
| 678 | // on a text object and which runs without their own language would then read. | |
| 679 | language: first.language.filter(|value| { | |
| 680 | target | |
| 681 | .spans() | |
| 682 | .iter() | |
| 683 | .all(|span| span.format.language == Some(*value)) | |
| 684 | && stored | |
| 685 | .spans() | |
| 686 | .iter() | |
| 687 | .any(|span| span.format.language != Some(*value)) | |
| 688 | }), | |
| 671 | 689 | }) |
| 672 | 690 | } |
| 673 | 691 | |
| ... | ... | @@ -986,6 +1004,54 @@ impl Lowering { |
| 986 | 1004 | })?; |
| 987 | 1005 | } |
| 988 | 1006 | } |
| 1007 | let files = |page: &Page| -> BTreeMap<ExGuid, crate::page::Attachment> { | |
| 1008 | page.objects | |
| 1009 | .iter() | |
| 1010 | .filter_map(|object| match object { | |
| 1011 | PageObject::Attachment(file) => Some((file.id, file.clone())), | |
| 1012 | _ => None, | |
| 1013 | }) | |
| 1014 | .collect() | |
| 1015 | }; | |
| 1016 | let stored = files(old.page); | |
| 1017 | for (id, file) in files(new.page) { | |
| 1018 | let Some(previous) = stored.get(&id) else { | |
| 1019 | continue; | |
| 1020 | }; | |
| 1021 | let (from, to) = (&previous.layout, &file.layout); | |
| 1022 | if ( | |
| 1023 | from.max_width, | |
| 1024 | from.max_height, | |
| 1025 | from.width_set_by_user, | |
| 1026 | from.reserved_width, | |
| 1027 | ) != ( | |
| 1028 | to.max_width, | |
| 1029 | to.max_height, | |
| 1030 | to.width_set_by_user, | |
| 1031 | to.reserved_width, | |
| 1032 | ) { | |
| 1033 | return Err(invalid("A file on the page keeps its extent")); | |
| 1034 | } | |
| 1035 | if (from.x, from.y) != (to.x, to.y) { | |
| 1036 | let (Some(x), Some(y)) = (to.x, to.y) else { | |
| 1037 | return Err(invalid("A file position needs both coordinates")); | |
| 1038 | }; | |
| 1039 | self.emit(PageOp::Outline { | |
| 1040 | object: id, | |
| 1041 | edit: crate::OutlineEdit::Position { x, y }, | |
| 1042 | })?; | |
| 1043 | } | |
| 1044 | if (&previous.filename, &previous.source_path, previous.size) | |
| 1045 | != (&file.filename, &file.source_path, file.size) | |
| 1046 | { | |
| 1047 | self.emit(PageOp::Attachment { | |
| 1048 | attachment: id, | |
| 1049 | filename: file.filename.clone(), | |
| 1050 | source_path: file.source_path.clone(), | |
| 1051 | size: file.size, | |
| 1052 | })?; | |
| 1053 | } | |
| 1054 | } | |
| 989 | 1055 | let inks = |page: &Page| -> BTreeMap<ExGuid, crate::page::Ink> { |
| 990 | 1056 | page.objects |
| 991 | 1057 | .iter() |
| ... | ... | @@ -1051,7 +1117,9 @@ impl Lowering { |
| 1051 | 1117 | ..(*outline).clone() |
| 1052 | 1118 | }) |
| 1053 | 1119 | } |
| 1054 | PageObject::Image(_) | PageObject::Ink(_) => (*object).clone(), | |
| 1120 | PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Ink(_) => { | |
| 1121 | (*object).clone() | |
| 1122 | } | |
| 1055 | 1123 | PageObject::Title(_) | PageObject::Unsupported(_) => { |
| 1056 | 1124 | return Err(invalid( |
| 1057 | 1125 | "Titles and unsupported objects cannot be edited through the page model", |
| ... | ... | @@ -1513,8 +1581,10 @@ impl Lowering { |
| 1513 | 1581 | } |
| 1514 | 1582 | let kept: BTreeSet<ExGuid> = new.page.objects.iter().map(PageObject::id).collect(); |
| 1515 | 1583 | for object in &old.page.objects { |
| 1516 | if matches!(object, PageObject::Image(_) | PageObject::Ink(_)) | |
| 1517 | && !kept.contains(&object.id()) | |
| 1584 | if matches!( | |
| 1585 | object, | |
| 1586 | PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Ink(_) | |
| 1587 | ) && !kept.contains(&object.id()) | |
| 1518 | 1588 | { |
| 1519 | 1589 | self.emit(PageOp::Delete { |
| 1520 | 1590 | object: object.id(), |
crates/onestore/src/op/mod.rs+3-3| ... | ... | @@ -114,7 +114,7 @@ pub enum PageOp { |
| 114 | 114 | right: ExGuid, |
| 115 | 115 | }, |
| 116 | 116 | /// Moves a subtree before a direct child of `parent`, or last; `None` names the page, |
| 117 | /// whose children are outlines, pictures and ink. A paragraph keeps its level where it | |
| 117 | /// whose children are outlines, pictures, files and ink. A paragraph keeps its level where it | |
| 118 | 118 | /// lies deeper than its new parent. |
| 119 | 119 | Move { |
| 120 | 120 | object: ExGuid, |
| ... | ... | @@ -163,8 +163,8 @@ pub enum PageOp { |
| 163 | 163 | tags: Vec<Tag>, |
| 164 | 164 | definitions: Vec<(ExGuid, Definition)>, |
| 165 | 165 | }, |
| 166 | /// Adds an outline with its paragraphs, a picture with its payload, or ink, before a | |
| 167 | /// page child or on top. | |
| 166 | /// Adds an outline with its paragraphs, a picture or file with its payloads, or ink, | |
| 167 | /// before a page child or on top. | |
| 168 | 168 | Add { |
| 169 | 169 | object: PageObject, |
| 170 | 170 | before: Option<ExGuid>, |
crates/onestore/src/op/model.rs+44-15| ... | ... | @@ -194,7 +194,9 @@ pub(crate) fn holds(page: &Page, id: ExGuid) -> bool { |
| 194 | 194 | .iter() |
| 195 | 195 | .any(|o| o.id == id || identities(&o.paragraphs).contains(&id)), |
| 196 | 196 | PageObject::Ink(ink) => ink.strokes.iter().any(|s| s.id == id), |
| 197 | PageObject::Image(_) | PageObject::Unsupported(_) => false, | |
| 197 | PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Unsupported(_) => { | |
| 198 | false | |
| 199 | } | |
| 198 | 200 | } |
| 199 | 201 | }) |
| 200 | 202 | } |
| ... | ... | @@ -926,6 +928,7 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 926 | 928 | OutlineEdit::Position { x, y } => { |
| 927 | 929 | let layout = match page.objects.iter_mut().find(|o| o.id() == *object) { |
| 928 | 930 | Some(PageObject::Ink(ink)) => &mut ink.layout, |
| 931 | Some(PageObject::Attachment(file)) => &mut file.layout, | |
| 929 | 932 | _ => &mut outline_mut(page, *object)?.layout, |
| 930 | 933 | }; |
| 931 | 934 | layout.x = Some(stored(*x)); |
| ... | ... | @@ -1090,12 +1093,29 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 1090 | 1093 | PageObject::Outline(outline) |
| 1091 | 1094 | } |
| 1092 | 1095 | PageObject::Ink(ink) => PageObject::Ink(inked(ink)), |
| 1096 | PageObject::Attachment(file) => { | |
| 1097 | let mut file = file.clone(); | |
| 1098 | let layout = &mut file.layout; | |
| 1099 | for value in [ | |
| 1100 | &mut layout.x, | |
| 1101 | &mut layout.y, | |
| 1102 | &mut layout.max_width, | |
| 1103 | &mut layout.max_height, | |
| 1104 | ] { | |
| 1105 | *value = value.map(stored); | |
| 1106 | } | |
| 1107 | layout.width_set_by_user = layout | |
| 1108 | .max_width | |
| 1109 | .map(|_| layout.width_set_by_user == Some(true)); | |
| 1110 | PageObject::Attachment(file) | |
| 1111 | } | |
| 1093 | 1112 | object => object.clone(), |
| 1094 | 1113 | }; |
| 1095 | 1114 | let outline = matches!(object, PageObject::Outline(_)); |
| 1096 | 1115 | let at = page_position(page, *before)?; |
| 1097 | 1116 | page.objects.insert(at, object); |
| 1098 | if outline { | |
| 1117 | // Placing an object before another moves it there, which retitles as a move does. | |
| 1118 | if outline || before.is_some() { | |
| 1099 | 1119 | retitle(page, None); |
| 1100 | 1120 | } |
| 1101 | 1121 | } |
| ... | ... | @@ -1123,19 +1143,7 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 1123 | 1143 | source_path, |
| 1124 | 1144 | size, |
| 1125 | 1145 | } => { |
| 1126 | let (path, index) = lists(page) | |
| 1127 | .into_iter() | |
| 1128 | .find_map(|(path, _, list)| { | |
| 1129 | list.iter() | |
| 1130 | .position(|p| matches!(&p.content, ParagraphContent::Attachment(a) if a.id == *attachment)) | |
| 1131 | .map(|index| (path, index)) | |
| 1132 | }) | |
| 1133 | .ok_or_else(|| invalid("The attachment is not on the page"))?; | |
| 1134 | let ParagraphContent::Attachment(stored_attachment) = | |
| 1135 | &mut list_at(page, &path)[index].content | |
| 1136 | else { | |
| 1137 | unreachable!() | |
| 1138 | }; | |
| 1146 | let stored_attachment = attachment_mut(page, *attachment)?; | |
| 1139 | 1147 | stored_attachment.filename = filename.clone(); |
| 1140 | 1148 | stored_attachment.source_path = source_path.clone(); |
| 1141 | 1149 | stored_attachment.size = size.map(|[w, h]| [stored(w), stored(h)]); |
| ... | ... | @@ -1305,6 +1313,27 @@ fn image_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Image, Err |
| 1305 | 1313 | Ok(image) |
| 1306 | 1314 | } |
| 1307 | 1315 | |
| 1316 | fn attachment_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Attachment, Error> { | |
| 1317 | if let Some(at) = page.objects.iter().position(|o| o.id() == id) { | |
| 1318 | let PageObject::Attachment(file) = &mut page.objects[at] else { | |
| 1319 | return Err(invalid("Select an attachment")); | |
| 1320 | }; | |
| 1321 | return Ok(file); | |
| 1322 | } | |
| 1323 | let (path, index) = lists(page) | |
| 1324 | .into_iter() | |
| 1325 | .find_map(|(path, _, list)| { | |
| 1326 | list.iter() | |
| 1327 | .position(|p| matches!(&p.content, ParagraphContent::Attachment(a) if a.id == id)) | |
| 1328 | .map(|index| (path, index)) | |
| 1329 | }) | |
| 1330 | .ok_or_else(|| invalid("The attachment is not on the page"))?; | |
| 1331 | let ParagraphContent::Attachment(file) = &mut list_at(page, &path)[index].content else { | |
| 1332 | unreachable!() | |
| 1333 | }; | |
| 1334 | Ok(file) | |
| 1335 | } | |
| 1336 | ||
| 1308 | 1337 | fn ink_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Ink, Error> { |
| 1309 | 1338 | if let Some(at) = page.objects.iter().position(|o| o.id() == id) { |
| 1310 | 1339 | let PageObject::Ink(ink) = &mut page.objects[at] else { |
crates/onestore/src/op/properties.rs+7-1| ... | ... | @@ -141,11 +141,17 @@ fn tag_definition_values(definition: &Definition) -> Result<Values, Error> { |
| 141 | 141 | else { |
| 142 | 142 | return Err(invalid("A note tag must reference a tag definition")); |
| 143 | 143 | }; |
| 144 | // NoteTagPropertyStatus: hasLabel, hasFontColor, hasHighlightColor and hasIcon, as | |
| 145 | // OneNote's Customize Tags writes them. | |
| 146 | let status = u32::from(label.is_some()) | |
| 147 | | u32::from(color.is_some()) << 1 | |
| 148 | | u32::from(highlight.is_some()) << 2 | |
| 149 | | u32::from(shape.is_some_and(|shape| shape != 0)) << 3; | |
| 144 | 150 | let mut values: Values = vec![ |
| 145 | 151 | (0x0c003473, vec![0]), |
| 146 | 152 | (0x10003463, action_type.unwrap_or(0).to_le_bytes().to_vec()), |
| 147 | 153 | (0x10003464, shape.unwrap_or(0).to_le_bytes().to_vec()), |
| 148 | (0x14003467, 0u32.to_le_bytes().to_vec()), | |
| 154 | (0x14003467, status.to_le_bytes().to_vec()), | |
| 149 | 155 | ]; |
| 150 | 156 | if let Some(label) = label { |
| 151 | 157 | values.push((0x1c003468, crate::create::string(label))); |
crates/onestore/src/op/tests.rs+18| ... | ... | @@ -1105,6 +1105,7 @@ pub(super) fn mutate(page: &mut Page, rng: &mut Rng, family: Family) -> bool { |
| 1105 | 1105 | filename: "notes.txt".into(), |
| 1106 | 1106 | source_path: Some(r"C:\notes.txt".into()), |
| 1107 | 1107 | size: None, |
| 1108 | layout: Layout::default(), | |
| 1108 | 1109 | bytes: Some(b"attached bytes".as_slice().into()), |
| 1109 | 1110 | preview: None, |
| 1110 | 1111 | recording: None, |
| ... | ... | @@ -2957,3 +2958,20 @@ fn rule_lines_store_what_onenote_stores() { |
| 2957 | 2958 | .unwrap(); |
| 2958 | 2959 | } |
| 2959 | 2960 | } |
| 2961 | ||
| 2962 | /// Text added without a language stores the writer's, and lowering it back must not write | |
| 2963 | /// LCID 0 on the text object, which runs later written without a language would read. | |
| 2964 | #[test] | |
| 2965 | fn an_unset_language_is_not_paragraph_formatting() { | |
| 2966 | let language = |language| Format { | |
| 2967 | language, | |
| 2968 | ..Default::default() | |
| 2969 | }; | |
| 2970 | let stored = Paragraph::new(String::new(), language(Some(0x409))); | |
| 2971 | let fields = |target| lower::paragraph_fields(&stored, &target).unwrap().language; | |
| 2972 | assert_eq!(fields(Paragraph::new("Fresh".into(), language(None))), None); | |
| 2973 | assert_eq!( | |
| 2974 | fields(Paragraph::new("Frais".into(), language(Some(0x40c)))), | |
| 2975 | Some(0x40c) | |
| 2976 | ); | |
| 2977 | } |
crates/onestore/src/outline.rs+8-5| ... | ... | @@ -13,8 +13,9 @@ use std::collections::{BTreeMap, BTreeSet}; |
| 13 | 13 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] |
| 14 | 14 | #[serde(deny_unknown_fields)] |
| 15 | 15 | pub enum OutlineEdit { |
| 16 | /// Moves an ordinary page outline to coordinates measured in points, or offsets a page's | |
| 17 | /// ink drawing from its strokes' coordinates by that much, as OneNote 2010 moves one. | |
| 16 | /// Moves an ordinary page outline or a file on the page to coordinates measured in | |
| 17 | /// points, or offsets a page's ink drawing from its strokes' coordinates by that much, | |
| 18 | /// as OneNote 2010 moves one. | |
| 18 | 19 | /// Refreshes automatic page titles when the leading outline changes. |
| 19 | 20 | Position { x: f32, y: f32 }, |
| 20 | 21 | /// Changes an ordinary outline's maximum width in points, at least 36. |
| ... | ... | @@ -65,12 +66,14 @@ impl OutlineEdit { |
| 65 | 66 | } |
| 66 | 67 | } |
| 67 | 68 | Self::Position { .. } => { |
| 68 | if !matches!(node.kind, Kind::Outline { .. } | Kind::Ink { .. }) | |
| 69 | || parents.get(&object).map(Vec::as_slice) != Some(pages.as_slice()) | |
| 69 | if !matches!( | |
| 70 | node.kind, | |
| 71 | Kind::Outline { .. } | Kind::Ink { .. } | Kind::Attachment { .. } | |
| 72 | ) || parents.get(&object).map(Vec::as_slice) != Some(pages.as_slice()) | |
| 70 | 73 | || !view.nodes[page].children.contains(&object) |
| 71 | 74 | { |
| 72 | 75 | return Err(invalid( |
| 73 | "Select an ordinary outline or ink drawing directly on the page", | |
| 76 | "Select an ordinary outline, file or ink drawing directly on the page", | |
| 74 | 77 | )); |
| 75 | 78 | } |
| 76 | 79 | } |
crates/onestore/src/page/mod.rs+16-1| ... | ... | @@ -179,6 +179,8 @@ pub enum PageObject { |
| 179 | 179 | Outline(Outline), |
| 180 | 180 | Title(Title), |
| 181 | 181 | Image(Image), |
| 182 | /// A file on a blank part of the page, as OneNote places one attached or dropped there. | |
| 183 | Attachment(Attachment), | |
| 182 | 184 | Ink(Ink), |
| 183 | 185 | Unsupported(Unsupported), |
| 184 | 186 | } |
| ... | ... | @@ -189,6 +191,7 @@ impl PageObject { |
| 189 | 191 | Self::Outline(value) => value.id, |
| 190 | 192 | Self::Title(value) => value.id, |
| 191 | 193 | Self::Image(value) => value.id, |
| 194 | Self::Attachment(value) => value.id, | |
| 192 | 195 | Self::Ink(value) => value.id, |
| 193 | 196 | Self::Unsupported(value) => value.id, |
| 194 | 197 | } |
| ... | ... | @@ -198,6 +201,7 @@ impl PageObject { |
| 198 | 201 | Self::Outline(value) => &value.layout, |
| 199 | 202 | Self::Title(value) => &value.layout, |
| 200 | 203 | Self::Image(value) => &value.layout, |
| 204 | Self::Attachment(value) => &value.layout, | |
| 201 | 205 | Self::Ink(value) => &value.layout, |
| 202 | 206 | Self::Unsupported(value) => &value.layout, |
| 203 | 207 | } |
| ... | ... | @@ -208,6 +212,7 @@ impl PageObject { |
| 208 | 212 | Self::Outline(value) => &mut value.layout, |
| 209 | 213 | Self::Title(value) => &mut value.layout, |
| 210 | 214 | Self::Image(value) => &mut value.layout, |
| 215 | Self::Attachment(value) => &mut value.layout, | |
| 211 | 216 | Self::Ink(value) => &mut value.layout, |
| 212 | 217 | Self::Unsupported(value) => &mut value.layout, |
| 213 | 218 | } |
| ... | ... | @@ -268,8 +273,11 @@ pub struct Attachment { |
| 268 | 273 | pub filename: String, |
| 269 | 274 | /// Where the file was inserted from, when recorded. |
| 270 | 275 | pub source_path: Option<String>, |
| 271 | /// The icon OneNote rendered for the file, when present. | |
| 276 | /// The icon's size in points. | |
| 272 | 277 | pub size: Option<[f32; 2]>, |
| 278 | /// On the page, where the file's column sits and its extent; a paragraph's file has none. | |
| 279 | #[serde(default)] | |
| 280 | pub layout: Layout, | |
| 273 | 281 | #[serde(with = "payload")] |
| 274 | 282 | pub bytes: Option<Arc<[u8]>>, |
| 275 | 283 | #[serde(with = "payload")] |
| ... | ... | @@ -301,6 +309,7 @@ impl PartialEq for Attachment { |
| 301 | 309 | && self.filename == other.filename |
| 302 | 310 | && self.source_path == other.source_path |
| 303 | 311 | && self.size == other.size |
| 312 | && self.layout == other.layout | |
| 304 | 313 | && self.recording == other.recording |
| 305 | 314 | } |
| 306 | 315 | } |
| ... | ... | @@ -343,6 +352,7 @@ impl Attachment { |
| 343 | 352 | filename: filename.clone().unwrap_or_default(), |
| 344 | 353 | source_path: source_path.clone(), |
| 345 | 354 | size: icon_width.zip(*icon_height).map(|(w, h)| [w, h]), |
| 355 | layout: node.layout.clone(), | |
| 346 | 356 | bytes: payload(container)?, |
| 347 | 357 | preview: payload(preview)?, |
| 348 | 358 | recording: recording_id.map(|id| Recording { |
| ... | ... | @@ -818,6 +828,11 @@ impl Page { |
| 818 | 828 | page.objects |
| 819 | 829 | .push(PageObject::Image(Image::read(revision, id, node)?)); |
| 820 | 830 | } |
| 831 | Kind::Attachment { .. } => { | |
| 832 | page.objects.push(PageObject::Attachment(Attachment::read( | |
| 833 | revision, id, node, | |
| 834 | )?)); | |
| 835 | } | |
| 821 | 836 | Kind::Ink { .. } => { |
| 822 | 837 | page.objects |
| 823 | 838 | .push(PageObject::Ink(Ink::read(revision, id, node)?)); |
crates/onestore/src/tree.rs+4-1| ... | ... | @@ -89,7 +89,10 @@ impl TreeEdit { |
| 89 | 89 | .get(1) |
| 90 | 90 | .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?; |
| 91 | 91 | let compatible = |parent: ExGuid| match view.nodes[&self.object].kind { |
| 92 | Kind::Outline { .. } | Kind::Image { .. } | Kind::Ink { .. } => parent == *page, | |
| 92 | Kind::Outline { .. } | |
| 93 | | Kind::Image { .. } | |
| 94 | | Kind::Attachment { .. } | |
| 95 | | Kind::Ink { .. } => parent == *page, | |
| 93 | 96 | Kind::Paragraph { .. } => matches!( |
| 94 | 97 | view.nodes[&parent].kind, |
| 95 | 98 | Kind::Outline { .. } |
crates/onestore/tests/page_attachments.rs+138| ... | ... | @@ -112,6 +112,7 @@ fn insert_attachment(section: &str, preview: Option<Arc<[u8]>>) -> Vec<u8> { |
| 112 | 112 | filename: "notes 🦀.txt".into(), |
| 113 | 113 | source_path: None, |
| 114 | 114 | size: Some([24.0, 24.0]), |
| 115 | layout: Default::default(), | |
| 115 | 116 | bytes: Some(Arc::from(PAYLOAD)), |
| 116 | 117 | preview: preview.clone(), |
| 117 | 118 | recording: None, |
| ... | ... | @@ -182,6 +183,7 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { |
| 182 | 183 | filename: "sub/dir.txt".into(), |
| 183 | 184 | source_path: None, |
| 184 | 185 | size: None, |
| 186 | layout: Default::default(), | |
| 185 | 187 | bytes: Some(Arc::from(PAYLOAD)), |
| 186 | 188 | preview: None, |
| 187 | 189 | recording: None, |
| ... | ... | @@ -230,3 +232,139 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { |
| 230 | 232 | .unwrap(); |
| 231 | 233 | } |
| 232 | 234 | } |
| 235 | ||
| 236 | const FLOATING: &[u8] = include_bytes!("../../../corpus/attachment-floating/native/files.one"); | |
| 237 | ||
| 238 | fn floating(page: &Page) -> Vec<&Attachment> { | |
| 239 | page.objects | |
| 240 | .iter() | |
| 241 | .filter_map(|object| match object { | |
| 242 | PageObject::Attachment(file) => Some(file), | |
| 243 | _ => None, | |
| 244 | }) | |
| 245 | .collect() | |
| 246 | } | |
| 247 | ||
| 248 | #[test] | |
| 249 | fn files_onenote_places_on_the_page_read_as_page_objects_sharing_one_payload() { | |
| 250 | let store = Store::parse(FLOATING).unwrap(); | |
| 251 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 252 | let document = Document::parse(&index).unwrap(); | |
| 253 | let (space, _) = document.pages().unwrap()[1]; | |
| 254 | let page = Page::from_space(&document, space).unwrap(); | |
| 255 | let files = floating(&page); | |
| 256 | let [moved, attached] = files.as_slice() else { | |
| 257 | panic!("{page:?}"); | |
| 258 | }; | |
| 259 | // Inserted through COM then dragged, and attached at a click on blank page. | |
| 260 | assert_eq!( | |
| 261 | [moved.layout.x, moved.layout.y], | |
| 262 | [Some(288.0), Some(140.40001)] | |
| 263 | ); | |
| 264 | assert_eq!(moved.layout.max_width, None); | |
| 265 | assert_eq!( | |
| 266 | [attached.layout.x, attached.layout.y], | |
| 267 | [Some(342.0), Some(284.4)] | |
| 268 | ); | |
| 269 | assert_eq!( | |
| 270 | [attached.layout.max_width, attached.layout.max_height], | |
| 271 | [Some(54.0), Some(62.999985)] | |
| 272 | ); | |
| 273 | for file in files { | |
| 274 | assert_eq!(file.filename, "float.txt"); | |
| 275 | assert_eq!( | |
| 276 | file.bytes.as_deref(), | |
| 277 | Some(b"Floating file, stored at page level.\r\n".as_slice()) | |
| 278 | ); | |
| 279 | assert_eq!(file.size, Some([24.0, 24.0])); | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | /// `ONESTORE_FLOATING_EXPORT` names a directory receiving the section for a cold reopen. | |
| 284 | #[test] | |
| 285 | fn a_file_on_the_page_is_attached_moved_renamed_and_removed_as_ops() { | |
| 286 | let source = onestore::create_section("files.one", "Floating", "Author").unwrap(); | |
| 287 | let (space, before) = first_page(&source); | |
| 288 | let (_, native) = first_page(NATIVE); | |
| 289 | let file = Attachment { | |
| 290 | id: new_id().unwrap(), | |
| 291 | filename: "float 🦀.txt".into(), | |
| 292 | source_path: Some(r"C:\inputs\float 🦀.txt".into()), | |
| 293 | size: Some([24.0, 24.0]), | |
| 294 | layout: onestore::document::Layout { | |
| 295 | x: Some(342.0), | |
| 296 | y: Some(284.4), | |
| 297 | max_width: Some(54.0), | |
| 298 | max_height: Some(63.0), | |
| 299 | width_set_by_user: Some(false), | |
| 300 | reserved_width: None, | |
| 301 | }, | |
| 302 | bytes: Some(Arc::from(PAYLOAD)), | |
| 303 | preview: attachments(&native)[0].preview.clone(), | |
| 304 | recording: None, | |
| 305 | }; | |
| 306 | let steps: [&dyn Fn(&mut Page); 4] = [ | |
| 307 | &|page| page.objects.push(PageObject::Attachment(file.clone())), | |
| 308 | &|page| { | |
| 309 | let PageObject::Attachment(file) = page.objects.last_mut().unwrap() else { | |
| 310 | panic!() | |
| 311 | }; | |
| 312 | [file.layout.x, file.layout.y] = [Some(126.0), Some(410.4)]; | |
| 313 | }, | |
| 314 | &|page| { | |
| 315 | let PageObject::Attachment(file) = page.objects.last_mut().unwrap() else { | |
| 316 | panic!() | |
| 317 | }; | |
| 318 | file.filename = "renamed.txt".into(); | |
| 319 | }, | |
| 320 | &|page| { | |
| 321 | page.objects.pop(); | |
| 322 | }, | |
| 323 | ]; | |
| 324 | let mut image = source.clone(); | |
| 325 | let mut expected = before.clone(); | |
| 326 | for (step, edit) in steps.iter().enumerate() { | |
| 327 | edit(&mut expected); | |
| 328 | let arena = onestore::Arena::default(); | |
| 329 | let section = onestore::Section::open(&arena, image.clone()).unwrap(); | |
| 330 | let stored = section.page(space).unwrap(); | |
| 331 | let lowered = onestore::op::lower_page(&stored, &expected).unwrap(); | |
| 332 | let mut predicted = stored.clone(); | |
| 333 | for op in &lowered { | |
| 334 | onestore::op::predict(&mut predicted, op).unwrap(); | |
| 335 | } | |
| 336 | image = ops::page_edited(&image, space, lowered).unwrap(); | |
| 337 | let read = page_in(&image, space); | |
| 338 | assert_eq!(read, predicted, "step {step}"); | |
| 339 | assert_eq!(read, expected, "step {step}"); | |
| 340 | if let [file] = floating(&read)[..] { | |
| 341 | assert_eq!(file.bytes.as_deref(), Some(PAYLOAD)); | |
| 342 | assert_eq!(file.preview, attachments(&native)[0].preview); | |
| 343 | } | |
| 344 | if step == 1 | |
| 345 | && let Some(directory) = std::env::var_os("ONESTORE_FLOATING_EXPORT") | |
| 346 | { | |
| 347 | let directory = std::path::PathBuf::from(directory); | |
| 348 | std::fs::create_dir_all(&directory).unwrap(); | |
| 349 | std::fs::write(directory.join("files.one"), &image).unwrap(); | |
| 350 | let file_id = Store::parse(&image).unwrap().header.file_id; | |
| 351 | std::fs::write( | |
| 352 | directory.join("Open Notebook.onetoc2"), | |
| 353 | onestore::create_table_of_contents( | |
| 354 | "Open Notebook.onetoc2", | |
| 355 | &[("files.one", file_id)], | |
| 356 | ) | |
| 357 | .unwrap(), | |
| 358 | ) | |
| 359 | .unwrap(); | |
| 360 | } | |
| 361 | } | |
| 362 | assert!(floating(&page_in(&image, space)).is_empty()); | |
| 363 | // A file on the page has a position; one in a paragraph has none. | |
| 364 | let mut unplaced = before.clone(); | |
| 365 | unplaced.objects.push(PageObject::Attachment(Attachment { | |
| 366 | layout: Default::default(), | |
| 367 | ..file.clone() | |
| 368 | })); | |
| 369 | assert!(ops::saved(&source, space, &unplaced).is_err()); | |
| 370 | } |
crates/onestore/tests/page_model.rs+4-1| ... | ... | @@ -60,7 +60,10 @@ fn identities(page: &Page) -> Vec<onestore::ExGuid> { |
| 60 | 60 | ids.extend(outline.unsupported.iter().map(|u| u.id)); |
| 61 | 61 | } |
| 62 | 62 | } |
| 63 | PageObject::Image(_) | PageObject::Ink(_) | PageObject::Unsupported(_) => {} | |
| 63 | PageObject::Image(_) | |
| 64 | | PageObject::Attachment(_) | |
| 65 | | PageObject::Ink(_) | |
| 66 | | PageObject::Unsupported(_) => {} | |
| 64 | 67 | } |
| 65 | 68 | } |
| 66 | 69 | ids |
crates/onestore/tests/protected.rs+1| ... | ... | @@ -175,6 +175,7 @@ fn a_protected_page_edit_is_stored_under_the_section_key() { |
| 175 | 175 | filename: "sealed.txt".into(), |
| 176 | 176 | source_path: None, |
| 177 | 177 | size: Some([24.0, 24.0]), |
| 178 | layout: Default::default(), | |
| 178 | 179 | bytes: Some(std::sync::Arc::from(&b"A payload that stays sealed"[..])), |
| 179 | 180 | preview: None, |
| 180 | 181 | recording: None, |
crates/onestore/tests/support/page_edits.rs+1| ... | ... | @@ -142,6 +142,7 @@ fn projection(page: &Page) -> String { |
| 142 | 142 | } |
| 143 | 143 | PageObject::Title(title) => out.push_str(&format!("title {}\n", title.id)), |
| 144 | 144 | PageObject::Image(image) => out.push_str(&format!("image {}\n", image.id)), |
| 145 | PageObject::Attachment(file) => out.push_str(&format!("file {}\n", file.id)), | |
| 145 | 146 | PageObject::Ink(ink) => out.push_str(&format!("ink {}\n", ink.id)), |
| 146 | 147 | PageObject::Unsupported(u) => out.push_str(&format!("unsupported {}\n", u.id)), |
| 147 | 148 | } |
crates/snowbound/src/art.rs-3| ... | ... | @@ -66,6 +66,3 @@ pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy"); |
| 66 | 66 | pub const SYNC_DONE: &[&str] = art!("icons/sync-done"); |
| 67 | 67 | pub const SYNC_ERROR: &[&str] = art!("icons/sync-error"); |
| 68 | 68 | pub const SYNC_OFFLINE: &[&str] = art!("icons/sync-offline"); |
| 69 | ||
| 70 | pub const TAG_REMEMBER: &[&str] = art!("tags/remember"); | |
| 71 | pub const TAG_DEFINITION: &[&str] = art!("tags/definition"); |
crates/snowbound/src/attachment.rs created+94| ... | ... | @@ -0,0 +1,94 @@ |
| 1 | //! Files on the page, as OneNote 2010 attaches, opens and saves them. | |
| 2 | ||
| 3 | use crate::{State, platform}; | |
| 4 | use onestore::page::Attachment; | |
| 5 | use std::{error::Error, path::Path}; | |
| 6 | ||
| 7 | /// The size OneNote 2010 shows a file's icon at, in points. | |
| 8 | const ICON_SIZE: [f32; 2] = [24.0, 24.0]; | |
| 9 | ||
| 10 | impl State { | |
| 11 | /// Attach File, or a file dropped at `at`, a window point: a copy of the file's bytes | |
| 12 | /// at the caret or at `at`, with the icon the system shows for it. | |
| 13 | pub(crate) fn attach( | |
| 14 | &mut self, | |
| 15 | path: &Path, | |
| 16 | at: Option<[f32; 2]>, | |
| 17 | ) -> Result<(), Box<dyn Error>> { | |
| 18 | if self.session.as_ref().is_some_and(crate::Session::read_only) { | |
| 19 | return Ok(()); | |
| 20 | } | |
| 21 | let (Some(name), Ok(bytes)) = ( | |
| 22 | path.file_name().and_then(|name| name.to_str()), | |
| 23 | std::fs::read(path), | |
| 24 | ) else { | |
| 25 | platform::alert("Couldn't attach the file", "Choose a file you can open."); | |
| 26 | return Ok(()); | |
| 27 | }; | |
| 28 | let file = Attachment { | |
| 29 | id: onestore::page::text::new_id()?, | |
| 30 | filename: name.to_owned(), | |
| 31 | source_path: path.to_str().map(str::to_owned), | |
| 32 | size: Some(ICON_SIZE), | |
| 33 | layout: Default::default(), | |
| 34 | bytes: Some(bytes.into()), | |
| 35 | preview: platform::file_icon(path).map(Into::into), | |
| 36 | recording: None, | |
| 37 | }; | |
| 38 | let response = match at { | |
| 39 | Some(point) => { | |
| 40 | let scale = self.ui.scale(); | |
| 41 | let corner = self.ui.rect(crate::page()).unwrap_or_default(); | |
| 42 | self.view.drop_attachment( | |
| 43 | [ | |
| 44 | (point[0] - corner[0]) * scale, | |
| 45 | (point[1] - corner[1]) * scale, | |
| 46 | ], | |
| 47 | file, | |
| 48 | )? | |
| 49 | } | |
| 50 | None => self.view.insert_attachment(file)?, | |
| 51 | }; | |
| 52 | self.respond(response); | |
| 53 | Ok(()) | |
| 54 | } | |
| 55 | ||
| 56 | /// Open: a copy of the file, in a folder of its own, with the system's application for it. | |
| 57 | pub(crate) fn open_attachment(&self, file: &Attachment) -> Result<(), Box<dyn Error>> { | |
| 58 | let Some(bytes) = &file.bytes else { | |
| 59 | unstored(); | |
| 60 | return Ok(()); | |
| 61 | }; | |
| 62 | let root = std::env::temp_dir().join("Snowbound Attachments"); | |
| 63 | std::fs::create_dir_all(&root)?; | |
| 64 | // A copy opened earlier may still be open, edited, in its application. | |
| 65 | let folder = (0..1000) | |
| 66 | .map(|index| root.join(index.to_string())) | |
| 67 | .find(|folder| std::fs::create_dir(folder).is_ok()) | |
| 68 | .ok_or("No folder is free for a copy of the file")?; | |
| 69 | let path = folder.join(&file.filename); | |
| 70 | std::fs::write(&path, bytes)?; | |
| 71 | platform::open_file(&path); | |
| 72 | Ok(()) | |
| 73 | } | |
| 74 | ||
| 75 | /// Save As: the file's bytes where the user chooses, under its name by default. | |
| 76 | pub(crate) fn save_attachment(&self, file: &Attachment) -> Result<(), Box<dyn Error>> { | |
| 77 | let Some(bytes) = &file.bytes else { | |
| 78 | unstored(); | |
| 79 | return Ok(()); | |
| 80 | }; | |
| 81 | if let Some(path) = platform::pick_new("Save As", &file.filename, "Save") { | |
| 82 | std::fs::write(path, bytes)?; | |
| 83 | } | |
| 84 | Ok(()) | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | /// A file another program stored beside the section rather than in it. | |
| 89 | fn unstored() { | |
| 90 | platform::alert( | |
| 91 | "Couldn't open the file", | |
| 92 | "Open this page in OneNote to open or save the file.", | |
| 93 | ); | |
| 94 | } |
crates/snowbound/src/commands.rs+82-69| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | //! chords on each platform, when it applies and what it does. |
| 3 | 3 | |
| 4 | 4 | use crate::{Command as Work, FONTS, HIGHLIGHTS, SIZES, State, page, platform, search}; |
| 5 | use canvas::editor::{Alignment, FormatState, Formatting, ListStyle, NoteTag, Toggle}; | |
| 5 | use canvas::editor::{Alignment, FormatState, Formatting, ListStyle, Toggle}; | |
| 6 | 6 | use canvas::interaction::Request; |
| 7 | 7 | use draw::edit::{Key, Modifiers, NamedKey, Platform}; |
| 8 | 8 | use std::{error::Error, sync::Arc}; |
| ... | ... | @@ -63,7 +63,9 @@ pub enum Id { |
| 63 | 63 | Indent, |
| 64 | 64 | Outdent, |
| 65 | 65 | ClearFormatting, |
| 66 | Tag(NoteTag), | |
| 66 | /// The tag at this place in the user's tag list. | |
| 67 | Tag(usize), | |
| 68 | CustomizeTags, | |
| 67 | 69 | RemoveTags, |
| 68 | 70 | FindTags, |
| 69 | 71 | Help, |
| ... | ... | @@ -176,15 +178,6 @@ macro_rules! row { |
| 176 | 178 | }; |
| 177 | 179 | } |
| 178 | 180 | |
| 179 | macro_rules! tag { | |
| 180 | ($tag:expr, $digit:literal) => { | |
| 181 | row!(Id::Tag($tag), $tag.label(), &[cmd($digit)], &[cmd($digit)]) | |
| 182 | }; | |
| 183 | ($tag:expr) => { | |
| 184 | row!(Id::Tag($tag), $tag.label(), NONE, NONE) | |
| 185 | }; | |
| 186 | } | |
| 187 | ||
| 188 | 181 | const NONE: &[Chord] = &[]; |
| 189 | 182 | |
| 190 | 183 | /// OneNote 2010's chords off macOS; there, AppKit's and OneNote for Mac's. |
| ... | ... | @@ -378,35 +371,7 @@ pub const COMMANDS: &[Command] = &[ |
| 378 | 371 | &[cmd('n').shift()], |
| 379 | 372 | &[cmd('n').shift()] |
| 380 | 373 | ), |
| 381 | tag!(NoteTag::ToDo, '1'), | |
| 382 | tag!(NoteTag::Important, '2'), | |
| 383 | tag!(NoteTag::Question, '3'), | |
| 384 | tag!(NoteTag::RememberForLater, '4'), | |
| 385 | tag!(NoteTag::Definition, '5'), | |
| 386 | tag!(NoteTag::Highlight, '6'), | |
| 387 | tag!(NoteTag::Contact, '7'), | |
| 388 | tag!(NoteTag::Address, '8'), | |
| 389 | tag!(NoteTag::PhoneNumber, '9'), | |
| 390 | tag!(NoteTag::WebSiteToVisit), | |
| 391 | tag!(NoteTag::Idea), | |
| 392 | tag!(NoteTag::Password), | |
| 393 | tag!(NoteTag::Critical), | |
| 394 | tag!(NoteTag::ProjectA), | |
| 395 | tag!(NoteTag::ProjectB), | |
| 396 | tag!(NoteTag::MovieToSee), | |
| 397 | tag!(NoteTag::BookToRead), | |
| 398 | tag!(NoteTag::MusicToListenTo), | |
| 399 | tag!(NoteTag::SourceForArticle), | |
| 400 | tag!(NoteTag::RememberForBlog), | |
| 401 | tag!(NoteTag::DiscussWithPersonA), | |
| 402 | tag!(NoteTag::DiscussWithPersonB), | |
| 403 | tag!(NoteTag::DiscussWithManager), | |
| 404 | tag!(NoteTag::SendInEmail), | |
| 405 | tag!(NoteTag::ScheduleMeeting), | |
| 406 | tag!(NoteTag::CallBack), | |
| 407 | tag!(NoteTag::ToDoPriority1), | |
| 408 | tag!(NoteTag::ToDoPriority2), | |
| 409 | tag!(NoteTag::ClientRequest), | |
| 374 | row!(Id::CustomizeTags, "Customize Tags…", NONE, NONE), | |
| 410 | 375 | row!( |
| 411 | 376 | Id::RemoveTags, |
| 412 | 377 | "Remove Tag", |
| ... | ... | @@ -417,6 +382,9 @@ pub const COMMANDS: &[Command] = &[ |
| 417 | 382 | row!(Id::Help, "Snowbound Help", NONE, NONE), |
| 418 | 383 | ]; |
| 419 | 384 | |
| 385 | /// The most tags a list holds: MS-ONE's action types 0 to 99 number them. | |
| 386 | pub const TAGS: usize = 100; | |
| 387 | ||
| 420 | 388 | /// Where Snowbound Help leads. |
| 421 | 389 | const HELP: &str = "https://shale.paperclover.net/snowbound"; |
| 422 | 390 | |
| ... | ... | @@ -443,6 +411,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { |
| 443 | 411 | .map(|command| Choice::Command(command.id)) |
| 444 | 412 | .chain(FONTS.map(|name| Choice::Font(name.to_owned()))) |
| 445 | 413 | .chain(SIZES.into_iter().map(Choice::Size)) |
| 414 | .chain((0..TAGS).map(|place| Choice::Command(Id::Tag(place)))) | |
| 446 | 415 | .chain(std::iter::once(Choice::Highlight(None))) |
| 447 | 416 | .chain( |
| 448 | 417 | HIGHLIGHTS |
| ... | ... | @@ -455,6 +424,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { |
| 455 | 424 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] |
| 456 | 425 | pub fn title(choice: Choice) -> String { |
| 457 | 426 | match choice { |
| 427 | Choice::Command(Id::Tag(_)) => unreachable!("The tag list names its tags"), | |
| 458 | 428 | Choice::Command(id) => command(id).title.to_owned(), |
| 459 | 429 | Choice::Font(name) => name, |
| 460 | 430 | Choice::Size(size) => format!("{size}"), |
| ... | ... | @@ -483,11 +453,30 @@ pub fn index(choice: &Choice) -> usize { |
| 483 | 453 | |
| 484 | 454 | /// The command `key` with `modifiers` runs here. |
| 485 | 455 | pub fn find(key: &Key, modifiers: Modifiers) -> Option<Id> { |
| 486 | let pressed = pressed(key, modifiers, Platform::CURRENT)?; | |
| 456 | ran( | |
| 457 | pressed(key, modifiers, Platform::CURRENT)?, | |
| 458 | Platform::CURRENT, | |
| 459 | ) | |
| 460 | } | |
| 461 | ||
| 462 | /// The command `chord` runs on `platform`. | |
| 463 | fn ran(chord: Chord, platform: Platform) -> Option<Id> { | |
| 487 | 464 | COMMANDS |
| 488 | 465 | .iter() |
| 489 | .find(|command| command.chords(Platform::CURRENT).contains(&pressed)) | |
| 466 | .find(|command| command.chords(platform).contains(&chord)) | |
| 490 | 467 | .map(|command| command.id) |
| 468 | .or_else(|| { | |
| 469 | (0..9) | |
| 470 | .find(|&place| tag_chord(place) == Some(chord)) | |
| 471 | .map(Id::Tag) | |
| 472 | }) | |
| 473 | } | |
| 474 | ||
| 475 | /// The chord applying the tag at `place` in the list: the first nine take Ctrl+1 to Ctrl+9, | |
| 476 | /// as OneNote's do. | |
| 477 | pub fn tag_chord(place: usize) -> Option<Chord> { | |
| 478 | let digit = u8::try_from(place).ok().filter(|place| *place < 9)?; | |
| 479 | Some(cmd(char::from(b'1' + digit))) | |
| 491 | 480 | } |
| 492 | 481 | |
| 493 | 482 | fn pressed(key: &Key, modifiers: Modifiers, platform: Platform) -> Option<Chord> { |
| ... | ... | @@ -599,10 +588,11 @@ impl Chord { |
| 599 | 588 | |
| 600 | 589 | /// The chord a menu or hint shows for `id` here, empty where it has none. |
| 601 | 590 | pub fn shortcut(id: Id) -> String { |
| 602 | command(id) | |
| 603 | .chords(Platform::CURRENT) | |
| 604 | .first() | |
| 605 | .map_or_else(String::new, |chord| chord.label(Platform::CURRENT)) | |
| 591 | let chord = match id { | |
| 592 | Id::Tag(place) => tag_chord(place), | |
| 593 | id => command(id).chords(Platform::CURRENT).first().copied(), | |
| 594 | }; | |
| 595 | chord.map_or_else(String::new, |chord| chord.label(Platform::CURRENT)) | |
| 606 | 596 | } |
| 607 | 597 | |
| 608 | 598 | impl State { |
| ... | ... | @@ -627,7 +617,7 @@ impl State { |
| 627 | 617 | pub(crate) fn status(&self, choice: &Choice, format: &FormatState) -> Status { |
| 628 | 618 | let session = self.session.as_ref(); |
| 629 | 619 | let welcome = session.is_none() && !self.temporary && self.sectionless.is_none(); |
| 630 | let modal = self.options.is_some() || self.link.is_some(); | |
| 620 | let modal = self.options.is_some() || self.link.is_some() || self.tag_list.is_some(); | |
| 631 | 621 | let page = (session.is_some() || self.temporary) && !modal; |
| 632 | 622 | let writable = page && !session.is_some_and(|session| session.read_only()); |
| 633 | 623 | let text = writable && self.view.accepts_text(); |
| ... | ... | @@ -655,9 +645,12 @@ impl State { |
| 655 | 645 | Choice::PageColor(_) | Choice::RuleLines(_) | Choice::Art(_) => Id::PageColor, |
| 656 | 646 | }; |
| 657 | 647 | match id { |
| 658 | Id::Settings | Id::NewNotebook | Id::OpenNotebook | Id::CommandPalette | Id::Help => { | |
| 659 | enabled(!modal) | |
| 660 | } | |
| 648 | Id::Settings | |
| 649 | | Id::NewNotebook | |
| 650 | | Id::OpenNotebook | |
| 651 | | Id::CommandPalette | |
| 652 | | Id::CustomizeTags | |
| 653 | | Id::Help => enabled(!modal), | |
| 661 | 654 | Id::CloseNotebook | Id::ShowNotebook => enabled(!modal && self.notebook().is_some()), |
| 662 | 655 | Id::NewSection | Id::NewSectionGroup => enabled( |
| 663 | 656 | !modal |
| ... | ... | @@ -715,14 +708,15 @@ impl State { |
| 715 | 708 | }, |
| 716 | 709 | Id::PageColor => enabled(writable && session.is_some()), |
| 717 | 710 | Id::ScreenClipping => enabled(text && cfg!(target_os = "macos")), |
| 718 | // Attach File awaits #23, Record Audio and Video #36. | |
| 719 | Id::Attachment | Id::RecordAudio | Id::RecordVideo => enabled(false), | |
| 711 | // Record Audio and Video await #36. | |
| 712 | Id::RecordAudio | Id::RecordVideo => enabled(false), | |
| 720 | 713 | Id::InsertSpace => Status { |
| 721 | 714 | enabled: writable, |
| 722 | 715 | checked: self.view.inserting_space(), |
| 723 | 716 | }, |
| 724 | 717 | Id::Table |
| 725 | 718 | | Id::Picture |
| 719 | | Id::Attachment | |
| 726 | 720 | | Id::Symbol |
| 727 | 721 | | Id::Link |
| 728 | 722 | | Id::Equation |
| ... | ... | @@ -739,7 +733,10 @@ impl State { |
| 739 | 733 | Id::Bullets => checked(format.bullets), |
| 740 | 734 | Id::Numbering => checked(format.numbering), |
| 741 | 735 | Id::Align(alignment) => checked(format.alignment == Some(alignment)), |
| 742 | Id::Tag(tag) => checked(format.tags.contains(&tag)), | |
| 736 | Id::Tag(place) => match self.tags.get(place) { | |
| 737 | Some(tag) => checked(format.tags.contains(&(tag.clone(), place as u16))), | |
| 738 | None => Status::default(), | |
| 739 | }, | |
| 743 | 740 | } |
| 744 | 741 | } |
| 745 | 742 | |
| ... | ... | @@ -815,14 +812,19 @@ impl State { |
| 815 | 812 | return Ok(()); |
| 816 | 813 | } |
| 817 | 814 | Choice::PageColor(color) => { |
| 818 | return self.paper_page(color, self.view.editor.rule_lines()); | |
| 815 | return self.paper_page(color, self.view.editor.rule_lines(), None); | |
| 819 | 816 | } |
| 820 | 817 | Choice::Art(name) => { |
| 821 | return self.art_page(name.and_then(canvas::template::find)); | |
| 818 | let editor = &self.view.editor; | |
| 819 | return self.paper_page( | |
| 820 | editor.page_color(), | |
| 821 | editor.rule_lines(), | |
| 822 | Some(name.and_then(canvas::template::find)), | |
| 823 | ); | |
| 822 | 824 | } |
| 823 | 825 | Choice::RuleLines(lines) => { |
| 824 | 826 | let lines = lines.map(|index| canvas::template::RULE_LINES[index].1); |
| 825 | return self.paper_page(self.view.editor.page_color(), lines); | |
| 827 | return self.paper_page(self.view.editor.page_color(), lines, None); | |
| 826 | 828 | } |
| 827 | 829 | }; |
| 828 | 830 | let field = self.ui.focused().filter(|focus| *focus != page()); |
| ... | ... | @@ -951,11 +953,18 @@ impl State { |
| 951 | 953 | return Ok(()); |
| 952 | 954 | } |
| 953 | 955 | Id::Picture => { |
| 954 | let Some(path) = platform::pick_picture() else { | |
| 956 | let Some(path) = platform::pick_file("Insert Picture", &crate::PICTURE_TYPES) | |
| 957 | else { | |
| 955 | 958 | return Ok(()); |
| 956 | 959 | }; |
| 957 | 960 | return self.insert_picture(std::fs::read(path)?); |
| 958 | 961 | } |
| 962 | Id::Attachment => { | |
| 963 | let Some(path) = platform::pick_file("Attach File", &[]) else { | |
| 964 | return Ok(()); | |
| 965 | }; | |
| 966 | return self.attach(&path, None); | |
| 967 | } | |
| 959 | 968 | Id::ScreenClipping => { |
| 960 | 969 | #[cfg(target_os = "macos")] |
| 961 | 970 | platform::clip_screen(self.proxy.clone()); |
| ... | ... | @@ -971,7 +980,7 @@ impl State { |
| 971 | 980 | self.save_settings(); |
| 972 | 981 | return Ok(()); |
| 973 | 982 | } |
| 974 | Id::Attachment | Id::RecordAudio | Id::RecordVideo => { | |
| 983 | Id::RecordAudio | Id::RecordVideo => { | |
| 975 | 984 | return Ok(()); |
| 976 | 985 | } |
| 977 | 986 | Id::InsertSpace => { |
| ... | ... | @@ -1016,7 +1025,14 @@ impl State { |
| 1016 | 1025 | Id::Indent => return format(self, Formatting::Indent), |
| 1017 | 1026 | Id::Outdent => return format(self, Formatting::Outdent), |
| 1018 | 1027 | Id::ClearFormatting => return format(self, Formatting::Clear), |
| 1019 | Id::Tag(tag) => return format(self, Formatting::Tag(tag)), | |
| 1028 | Id::Tag(place) => { | |
| 1029 | let tag = self.tags[place].clone(); | |
| 1030 | return format(self, Formatting::Tag(tag, place as u16)); | |
| 1031 | } | |
| 1032 | Id::CustomizeTags => { | |
| 1033 | self.open_customize_tags(); | |
| 1034 | return Ok(()); | |
| 1035 | } | |
| 1020 | 1036 | Id::RemoveTags => return format(self, Formatting::RemoveTags), |
| 1021 | 1037 | }; |
| 1022 | 1038 | self.commands.push(response); |
| ... | ... | @@ -1039,8 +1055,9 @@ mod tests { |
| 1039 | 1055 | command |
| 1040 | 1056 | .chords(platform) |
| 1041 | 1057 | .iter() |
| 1042 | .map(move |chord| (chord, command.title)) | |
| 1058 | .map(move |chord| (*chord, command.title)) | |
| 1043 | 1059 | }) |
| 1060 | .chain((0..9).filter_map(|place| Some((tag_chord(place)?, "a tag")))) | |
| 1044 | 1061 | .collect(); |
| 1045 | 1062 | for (at, (chord, title)) in chords.iter().enumerate() { |
| 1046 | 1063 | if let Some((_, other)) = |
| ... | ... | @@ -1081,11 +1098,10 @@ mod tests { |
| 1081 | 1098 | control, |
| 1082 | 1099 | command, |
| 1083 | 1100 | }; |
| 1084 | let pressed = pressed(&Key::Character(key.into()), modifiers, platform)?; | |
| 1085 | COMMANDS | |
| 1086 | .iter() | |
| 1087 | .find(|listed| listed.chords(platform).contains(&pressed)) | |
| 1088 | .map(|listed| listed.id) | |
| 1101 | ran( | |
| 1102 | pressed(&Key::Character(key.into()), modifiers, platform)?, | |
| 1103 | platform, | |
| 1104 | ) | |
| 1089 | 1105 | }; |
| 1090 | 1106 | let mac = Platform::MacOs; |
| 1091 | 1107 | assert_eq!( |
| ... | ... | @@ -1110,10 +1126,7 @@ mod tests { |
| 1110 | 1126 | find(mac, "0", false, false, true, true), |
| 1111 | 1127 | Some(Id::RemoveTags) |
| 1112 | 1128 | ); |
| 1113 | assert_eq!( | |
| 1114 | find(mac, "7", false, false, false, true), | |
| 1115 | Some(Id::Tag(NoteTag::Contact)) | |
| 1116 | ); | |
| 1129 | assert_eq!(find(mac, "7", false, false, false, true), Some(Id::Tag(6))); | |
| 1117 | 1130 | assert_eq!(find(mac, "q", false, false, false, true), None); |
| 1118 | 1131 | // The highlighter's chord runs the command that applies its last pick. |
| 1119 | 1132 | assert_eq!( |
crates/snowbound/src/link.rs+48-40| ... | ... | @@ -275,7 +275,7 @@ impl State { |
| 275 | 275 | Ok(()) |
| 276 | 276 | } |
| 277 | 277 | |
| 278 | /// A secondary press on the page's text opens its context menu there. | |
| 278 | /// A secondary press on the page's text or a file opens its context menu there. | |
| 279 | 279 | pub(crate) fn open_text_menu(&mut self) -> Result<(), Box<dyn Error>> { |
| 280 | 280 | let Some((response, context)) = self.view.context()? else { |
| 281 | 281 | return Ok(()); |
| ... | ... | @@ -288,7 +288,7 @@ impl State { |
| 288 | 288 | } |
| 289 | 289 | |
| 290 | 290 | /// The page's context menu while it is open, with OneNote 2010's commands for text, |
| 291 | /// links and equations. | |
| 291 | /// links, equations and files. | |
| 292 | 292 | pub(crate) fn text_menu(&mut self) -> Result<(), Box<dyn Error>> { |
| 293 | 293 | let Some((context, point)) = &self.text_menu else { |
| 294 | 294 | return Ok(()); |
| ... | ... | @@ -301,48 +301,54 @@ impl State { |
| 301 | 301 | text, |
| 302 | 302 | ..Item::default() |
| 303 | 303 | }; |
| 304 | let mut items = vec![ | |
| 305 | Item { | |
| 306 | disabled: !context.selected, | |
| 307 | ..item("Cut") | |
| 308 | }, | |
| 309 | Item { | |
| 310 | disabled: !context.selected, | |
| 311 | ..item("Copy") | |
| 312 | }, | |
| 313 | item("Paste"), | |
| 314 | ]; | |
| 315 | match &context.link { | |
| 316 | Some(_) => items.extend([ | |
| 317 | Item { | |
| 318 | separated: true, | |
| 319 | ..item("Edit Link…") | |
| 320 | }, | |
| 321 | item("Copy Link to Paragraph"), | |
| 322 | Item { | |
| 323 | separated: true, | |
| 324 | ..item("Copy Link") | |
| 325 | }, | |
| 326 | item("Select Link"), | |
| 327 | item("Remove Link"), | |
| 328 | ]), | |
| 329 | None => items.extend([ | |
| 304 | let items = if context.attachment.is_some() { | |
| 305 | // OneNote 2010's commands for the file itself; its clipboard holds text alone. | |
| 306 | vec![item("Open"), item("Save As…")] | |
| 307 | } else { | |
| 308 | let mut items = vec![ | |
| 330 | 309 | Item { |
| 331 | separated: true, | |
| 332 | ..item("Link…") | |
| 310 | disabled: !context.selected, | |
| 311 | ..item("Cut") | |
| 333 | 312 | }, |
| 334 | item("Copy Link to Paragraph"), | |
| 335 | ]), | |
| 336 | } | |
| 337 | if context.equation { | |
| 338 | items.extend([ | |
| 339 | 313 | Item { |
| 340 | separated: true, | |
| 341 | ..item("Professional") | |
| 314 | disabled: !context.selected, | |
| 315 | ..item("Copy") | |
| 342 | 316 | }, |
| 343 | item("Linear"), | |
| 344 | ]); | |
| 345 | } | |
| 317 | item("Paste"), | |
| 318 | ]; | |
| 319 | match &context.link { | |
| 320 | Some(_) => items.extend([ | |
| 321 | Item { | |
| 322 | separated: true, | |
| 323 | ..item("Edit Link…") | |
| 324 | }, | |
| 325 | item("Copy Link to Paragraph"), | |
| 326 | Item { | |
| 327 | separated: true, | |
| 328 | ..item("Copy Link") | |
| 329 | }, | |
| 330 | item("Select Link"), | |
| 331 | item("Remove Link"), | |
| 332 | ]), | |
| 333 | None => items.extend([ | |
| 334 | Item { | |
| 335 | separated: true, | |
| 336 | ..item("Link…") | |
| 337 | }, | |
| 338 | item("Copy Link to Paragraph"), | |
| 339 | ]), | |
| 340 | } | |
| 341 | if context.equation { | |
| 342 | items.extend([ | |
| 343 | Item { | |
| 344 | separated: true, | |
| 345 | ..item("Professional") | |
| 346 | }, | |
| 347 | item("Linear"), | |
| 348 | ]); | |
| 349 | } | |
| 350 | items | |
| 351 | }; | |
| 346 | 352 | let Some(chosen) = |
| 347 | 353 | ui::popup::menu(&mut self.ui, menu(), Anchor::Point(*point), &items, None) |
| 348 | 354 | else { |
| ... | ... | @@ -352,6 +358,8 @@ impl State { |
| 352 | 358 | return Ok(()); |
| 353 | 359 | }; |
| 354 | 360 | let response = match items[chosen].text { |
| 361 | "Open" => return self.open_attachment(&context.attachment.unwrap()), | |
| 362 | "Save As…" => return self.save_attachment(&context.attachment.unwrap()), | |
| 355 | 363 | "Cut" => self.view.copy(true)?, |
| 356 | 364 | "Copy" => self.view.copy(false)?, |
| 357 | 365 | "Paste" => { |
crates/snowbound/src/linux.rs+32-16| ... | ... | @@ -593,6 +593,8 @@ pub fn install_menu() {} |
| 593 | 593 | /// The toolbar and keyboard stand in for a menu bar. |
| 594 | 594 | pub fn update_menu(_: impl FnOnce() -> Vec<crate::commands::Status>) {} |
| 595 | 595 | |
| 596 | pub fn update_tag_menu(_: &[canvas::editor::NoteTag]) {} | |
| 597 | ||
| 596 | 598 | pub fn clear_marked_text(_: &Window) {} |
| 597 | 599 | |
| 598 | 600 | #[cfg(feature = "wgpu")] |
| ... | ... | @@ -1056,24 +1058,23 @@ fn dialog<const Z: usize, const K: usize>( |
| 1056 | 1058 | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) |
| 1057 | 1059 | } |
| 1058 | 1060 | |
| 1059 | /// Asks for a picture to insert. | |
| 1060 | pub fn pick_picture() -> Option<PathBuf> { | |
| 1061 | let patterns = crate::PICTURE_TYPES | |
| 1062 | .map(|kind| format!("*.{kind}")) | |
| 1063 | .join(" "); | |
| 1061 | /// Asks for a file to insert, one of `types` (extensions) unless empty, titled `title`. | |
| 1062 | pub fn pick_file(title: &str, types: &[&str]) -> Option<PathBuf> { | |
| 1063 | let patterns = match types { | |
| 1064 | [] => "*".to_owned(), | |
| 1065 | types => types | |
| 1066 | .iter() | |
| 1067 | .map(|kind| format!("*.{kind}")) | |
| 1068 | .collect::<Vec<_>>() | |
| 1069 | .join(" "), | |
| 1070 | }; | |
| 1064 | 1071 | let asked = dialog( |
| 1065 | 1072 | [ |
| 1066 | 1073 | "--file-selection", |
| 1067 | "--title=Insert Picture", | |
| 1068 | &format!("--file-filter=Pictures | {patterns}"), | |
| 1069 | ], | |
| 1070 | [ | |
| 1071 | "--getopenfilename", | |
| 1072 | ".", | |
| 1073 | &patterns, | |
| 1074 | "--title", | |
| 1075 | "Insert Picture", | |
| 1074 | &format!("--title={title}"), | |
| 1075 | &format!("--file-filter={patterns}"), | |
| 1076 | 1076 | ], |
| 1077 | ["--getopenfilename", ".", &patterns, "--title", title], | |
| 1077 | 1078 | ); |
| 1078 | 1079 | asked |
| 1079 | 1080 | .unwrap_or_else(|error| { |
| ... | ... | @@ -1084,6 +1085,21 @@ pub fn pick_picture() -> Option<PathBuf> { |
| 1084 | 1085 | .map(PathBuf::from) |
| 1085 | 1086 | } |
| 1086 | 1087 | |
| 1088 | /// No icon theme lookup; the page draws a blank page for the file. | |
| 1089 | pub fn file_icon(_: &std::path::Path) -> Option<Vec<u8>> { | |
| 1090 | None | |
| 1091 | } | |
| 1092 | ||
| 1093 | /// winit reports drops without a position; the file goes to the caret. | |
| 1094 | pub fn drop_point(_: &Window) -> Option<[f32; 2]> { | |
| 1095 | None | |
| 1096 | } | |
| 1097 | ||
| 1098 | /// Opens a copy of an attachment with the desktop's application for it. | |
| 1099 | pub fn open_file(path: &std::path::Path) { | |
| 1100 | reveal(path); | |
| 1101 | } | |
| 1102 | ||
| 1087 | 1103 | /// Opens GNOME's character map, whose picks the user copies in. |
| 1088 | 1104 | pub fn character_palette() { |
| 1089 | 1105 | if let Err(error) = Command::new("gnome-characters").spawn() { |
| ... | ... | @@ -1151,8 +1167,8 @@ pub fn pick_notebook(title: &str) -> Option<PathBuf> { |
| 1151 | 1167 | .map(PathBuf::from) |
| 1152 | 1168 | } |
| 1153 | 1169 | |
| 1154 | /// Asks where to create something named `name` by default. | |
| 1155 | pub fn pick_new(title: &str, name: &str) -> Option<PathBuf> { | |
| 1170 | /// Asks where to put something named `name` by default; the dialog names its own button. | |
| 1171 | pub fn pick_new(title: &str, name: &str, _action: &str) -> Option<PathBuf> { | |
| 1156 | 1172 | let asked = dialog( |
| 1157 | 1173 | [ |
| 1158 | 1174 | "--file-selection", |
crates/snowbound/src/macos.rs+108-9| ... | ... | @@ -11,7 +11,7 @@ use objc2::{ |
| 11 | 11 | use objc2_app_kit::{ |
| 12 | 12 | NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSApplication, NSColor, |
| 13 | 13 | NSColorSpace, NSDatePicker, NSDatePickerElementFlags, NSDatePickerStyle, NSEvent, NSEventType, |
| 14 | NSMenuItem, | |
| 14 | NSMenu, NSMenuItem, | |
| 15 | 15 | }; |
| 16 | 16 | use objc2_foundation::{ |
| 17 | 17 | MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter, |
| ... | ... | @@ -38,6 +38,7 @@ static DELEGATE_CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); |
| 38 | 38 | thread_local! { |
| 39 | 39 | static IN_KEY_DOWN: Cell<bool> = const { Cell::new(false) }; |
| 40 | 40 | static STATUSES: RefCell<Vec<commands::Status>> = const { RefCell::new(Vec::new()) }; |
| 41 | static TAGS_MENU: RefCell<Option<Retained<NSMenu>>> = const { RefCell::new(None) }; | |
| 41 | 42 | } |
| 42 | 43 | |
| 43 | 44 | unsafe extern "C" fn key_down(view: &AnyObject, _: Sel, event: &NSEvent) { |
| ... | ... | @@ -427,14 +428,14 @@ pub fn pick_notebook(title: &str) -> Option<std::path::PathBuf> { |
| 427 | 428 | } |
| 428 | 429 | } |
| 429 | 430 | |
| 430 | /// Asks where to create something named `name` by default, with the system's save panel. | |
| 431 | pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { | |
| 431 | /// Asks where to `action` something named `name` by default, with the system's save panel. | |
| 432 | pub fn pick_new(title: &str, name: &str, action: &str) -> Option<std::path::PathBuf> { | |
| 432 | 433 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); |
| 433 | 434 | unsafe { |
| 434 | 435 | let panel = objc2_app_kit::NSSavePanel::savePanel(mtm); |
| 435 | 436 | panel.setCanCreateDirectories(true); |
| 436 | 437 | panel.setTitle(Some(&NSString::from_str(title))); |
| 437 | panel.setPrompt(Some(&NSString::from_str("Create"))); | |
| 438 | panel.setPrompt(Some(&NSString::from_str(action))); | |
| 438 | 439 | panel.setNameFieldStringValue(&NSString::from_str(name)); |
| 439 | 440 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { |
| 440 | 441 | return None; |
| ... | ... | @@ -443,14 +444,18 @@ pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { |
| 443 | 444 | } |
| 444 | 445 | } |
| 445 | 446 | |
| 446 | /// Asks for a picture to insert, with the system's open panel. | |
| 447 | pub fn pick_picture() -> Option<std::path::PathBuf> { | |
| 447 | /// Asks for a file to insert, one of `types` (extensions) unless empty, with the system's | |
| 448 | /// open panel titled `title`. | |
| 449 | pub fn pick_file(title: &str, types: &[&str]) -> Option<std::path::PathBuf> { | |
| 448 | 450 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); |
| 449 | 451 | unsafe { |
| 450 | 452 | let panel = objc2_app_kit::NSOpenPanel::openPanel(mtm); |
| 451 | let types = crate::PICTURE_TYPES.map(NSString::from_str); | |
| 452 | #[allow(deprecated, reason = "Allowed content types need macOS 11")] | |
| 453 | panel.setAllowedFileTypes(Some(&objc2_foundation::NSArray::from_vec(types.into()))); | |
| 453 | if !types.is_empty() { | |
| 454 | let types = types.iter().map(|kind| NSString::from_str(kind)).collect(); | |
| 455 | #[allow(deprecated, reason = "Allowed content types need macOS 11")] | |
| 456 | panel.setAllowedFileTypes(Some(&objc2_foundation::NSArray::from_vec(types))); | |
| 457 | } | |
| 458 | panel.setTitle(Some(&NSString::from_str(title))); | |
| 454 | 459 | panel.setPrompt(Some(&NSString::from_str("Insert"))); |
| 455 | 460 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { |
| 456 | 461 | return None; |
| ... | ... | @@ -460,6 +465,88 @@ pub fn pick_picture() -> Option<std::path::PathBuf> { |
| 460 | 465 | } |
| 461 | 466 | } |
| 462 | 467 | |
| 468 | /// The icon Finder shows for the file at `path`, as a 32 pixel PNG like the one OneNote | |
| 469 | /// 2010 stores with an attachment. | |
| 470 | pub fn file_icon(path: &std::path::Path) -> Option<Vec<u8>> { | |
| 471 | MainThreadMarker::new().expect("AppKit draws on the main thread"); | |
| 472 | let class = |name| AnyClass::get(name).expect("AppKit is linked"); | |
| 473 | unsafe { | |
| 474 | let workspace: Retained<AnyObject> = msg_send_id![class("NSWorkspace"), sharedWorkspace]; | |
| 475 | let icon: Retained<AnyObject> = | |
| 476 | msg_send_id![&workspace, iconForFile: &*NSString::from_str(path.to_str()?)]; | |
| 477 | let bitmap: Allocated<AnyObject> = msg_send_id![class("NSBitmapImageRep"), alloc]; | |
| 478 | let bitmap: Option<Retained<AnyObject>> = msg_send_id![ | |
| 479 | bitmap, | |
| 480 | initWithBitmapDataPlanes: std::ptr::null_mut::<*mut u8>(), | |
| 481 | pixelsWide: 32isize, | |
| 482 | pixelsHigh: 32isize, | |
| 483 | bitsPerSample: 8isize, | |
| 484 | samplesPerPixel: 4isize, | |
| 485 | hasAlpha: true, | |
| 486 | isPlanar: false, | |
| 487 | colorSpaceName: &*NSString::from_str("NSDeviceRGBColorSpace"), | |
| 488 | bytesPerRow: 0isize, | |
| 489 | bitsPerPixel: 0isize | |
| 490 | ]; | |
| 491 | let bitmap = bitmap?; | |
| 492 | let graphics = class("NSGraphicsContext"); | |
| 493 | let context: Option<Retained<AnyObject>> = | |
| 494 | msg_send_id![graphics, graphicsContextWithBitmapImageRep: &*bitmap]; | |
| 495 | let _: () = msg_send![graphics, saveGraphicsState]; | |
| 496 | let _: () = msg_send![graphics, setCurrentContext: context.as_deref()]; | |
| 497 | let rect = NSRect::new( | |
| 498 | NSPoint::new(0.0, 0.0), | |
| 499 | objc2_foundation::NSSize::new(32.0, 32.0), | |
| 500 | ); | |
| 501 | // NSCompositingOperationSourceOver. | |
| 502 | let _: () = msg_send![&icon, drawInRect: rect, fromRect: NSRect::ZERO, operation: 2usize, fraction: 1.0f64]; | |
| 503 | let _: () = msg_send![graphics, restoreGraphicsState]; | |
| 504 | let properties: Retained<AnyObject> = msg_send_id![class("NSDictionary"), dictionary]; | |
| 505 | // NSBitmapImageFileTypePNG. | |
| 506 | let data: Option<Retained<AnyObject>> = | |
| 507 | msg_send_id![&bitmap, representationUsingType: 4usize, properties: &*properties]; | |
| 508 | let data = data?; | |
| 509 | let length: usize = msg_send![&data, length]; | |
| 510 | let bytes: *const std::ffi::c_void = msg_send![&data, bytes]; | |
| 511 | Some(std::slice::from_raw_parts(bytes.cast::<u8>(), length).to_vec()) | |
| 512 | } | |
| 513 | } | |
| 514 | ||
| 515 | /// Where the pointer is in the window, in points from the content's top left corner, for a | |
| 516 | /// drop, which AppKit reports without a position. | |
| 517 | pub fn drop_point(window: &Window) -> Option<[f32; 2]> { | |
| 518 | let window = ns_window(window); | |
| 519 | unsafe { | |
| 520 | let point: NSPoint = msg_send![&window, mouseLocationOutsideOfEventStream]; | |
| 521 | let content: Retained<AnyObject> = msg_send_id![&window, contentView]; | |
| 522 | let frame: NSRect = msg_send![&content, frame]; | |
| 523 | Some([point.x as f32, (frame.size.height - point.y) as f32]) | |
| 524 | } | |
| 525 | } | |
| 526 | ||
| 527 | /// Opens a copy of an attachment with its application, quarantined as a download is, so | |
| 528 | /// Gatekeeper checks it where OneNote 2010 warns before opening one. | |
| 529 | pub fn open_file(path: &std::path::Path) { | |
| 530 | use std::os::unix::ffi::OsStrExt; | |
| 531 | let seconds = std::time::SystemTime::now() | |
| 532 | .duration_since(std::time::UNIX_EPOCH) | |
| 533 | .map_or(0, |since| since.as_secs()); | |
| 534 | let value = format!("0081;{seconds:08x};Snowbound;"); | |
| 535 | if let Ok(file) = std::ffi::CString::new(path.as_os_str().as_bytes()) { | |
| 536 | unsafe { | |
| 537 | libc::setxattr( | |
| 538 | file.as_ptr(), | |
| 539 | c"com.apple.quarantine".as_ptr(), | |
| 540 | value.as_ptr().cast(), | |
| 541 | value.len(), | |
| 542 | 0, | |
| 543 | 0, | |
| 544 | ); | |
| 545 | } | |
| 546 | } | |
| 547 | reveal(path); | |
| 548 | } | |
| 549 | ||
| 463 | 550 | /// Lets the user drag out part of the screen, then sends it as a PNG. |
| 464 | 551 | pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { |
| 465 | 552 | std::thread::spawn(move || { |
| ... | ... | @@ -910,6 +997,18 @@ pub fn install_menu() { |
| 910 | 997 | app.setWindowsMenu(Some(&menus.window)); |
| 911 | 998 | app.setHelpMenu(Some(&menus.help)); |
| 912 | 999 | } |
| 1000 | TAGS_MENU.set(Some(menus.tags)); | |
| 1001 | } | |
| 1002 | ||
| 1003 | /// Lists `tags` in the menu bar's Tags menu. | |
| 1004 | pub fn update_tag_menu(tags: &[canvas::editor::NoteTag]) { | |
| 1005 | let mtm = MainThreadMarker::new().expect("Menus belong to the main thread"); | |
| 1006 | let app = NSApplication::sharedApplication(mtm); | |
| 1007 | TAGS_MENU.with_borrow(|menu| { | |
| 1008 | if let Some(menu) = menu { | |
| 1009 | crate::menubar::tags(mtm, &app, menu, tags); | |
| 1010 | } | |
| 1011 | }); | |
| 913 | 1012 | } |
| 914 | 1013 | |
| 915 | 1014 | /// The table's statuses, in `commands::choices` order, which the menu bar's items show. |
crates/snowbound/src/main.rs+120-33| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | #[cfg(target_os = "macos")] |
| 2 | 2 | mod aqua; |
| 3 | 3 | mod art; |
| 4 | mod attachment; | |
| 4 | 5 | mod background; |
| 5 | 6 | mod commands; |
| 6 | 7 | #[cfg(test)] |
| ... | ... | @@ -28,6 +29,7 @@ mod sidebar; |
| 28 | 29 | #[cfg_attr(not(feature = "wgpu"), path = "surface_gl.rs")] |
| 29 | 30 | mod surface; |
| 30 | 31 | mod sync; |
| 32 | mod tags; | |
| 31 | 33 | mod templates; |
| 32 | 34 | |
| 33 | 35 | use canvas::gpu::page::PageScene; |
| ... | ... | @@ -524,6 +526,10 @@ struct State { |
| 524 | 526 | options: Option<options::Options>, |
| 525 | 527 | /// The Link dialog's fields while it is open. |
| 526 | 528 | link: Option<link::LinkDialog>, |
| 529 | /// The user's tag list, which the toolbar, menus and Ctrl+1 to Ctrl+9 apply. | |
| 530 | tags: Vec<canvas::editor::NoteTag>, | |
| 531 | /// The Customize Tags dialog's list while it is open. | |
| 532 | tag_list: Option<tags::TagList>, | |
| 527 | 533 | /// The page's context menu while it is open: what it was opened on, and where. |
| 528 | 534 | text_menu: Option<(canvas::interaction::Context, [f32; 2])>, |
| 529 | 535 | color_scheme: settings::ColorScheme, |
| ... | ... | @@ -815,6 +821,10 @@ impl State { |
| 815 | 821 | title_focus: None, |
| 816 | 822 | menu: None, |
| 817 | 823 | link: None, |
| 824 | tags: stored | |
| 825 | .tags | |
| 826 | .unwrap_or_else(canvas::editor::NoteTag::defaults), | |
| 827 | tag_list: None, | |
| 818 | 828 | text_menu: None, |
| 819 | 829 | options: None, |
| 820 | 830 | color_scheme: stored.color_scheme, |
| ... | ... | @@ -860,6 +870,7 @@ impl State { |
| 860 | 870 | accessibility: accessibility::Accessibility::default(), |
| 861 | 871 | }; |
| 862 | 872 | state.title(); |
| 873 | platform::update_tag_menu(&state.tags); | |
| 863 | 874 | Ok(state) |
| 864 | 875 | } |
| 865 | 876 | |
| ... | ... | @@ -930,6 +941,7 @@ impl State { |
| 930 | 941 | let (section, open_tab, open_page) = self.build()?; |
| 931 | 942 | self.options_dialog(); |
| 932 | 943 | self.link_dialog()?; |
| 944 | self.customize_tags(); | |
| 933 | 945 | self.palette(); |
| 934 | 946 | self.sync_popup()?; |
| 935 | 947 | self.text_menu()?; |
| ... | ... | @@ -1468,21 +1480,16 @@ impl State { |
| 1468 | 1480 | /// taking notes fold last, and those with chords or a place in the menu bar first. |
| 1469 | 1481 | fn tools(&mut self, theme: &Theme) { |
| 1470 | 1482 | use Entry::{Open, Rule, Run, Soon}; |
| 1471 | use canvas::editor::{ | |
| 1472 | Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, NoteTag, Toggle, | |
| 1473 | }; | |
| 1483 | use canvas::editor::{Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, Toggle}; | |
| 1474 | 1484 | use commands::{Choice, Id as Cmd}; |
| 1475 | 1485 | use ui::shell::TOOL; |
| 1476 | 1486 | // A focused picture has no text to show a format for. |
| 1477 | 1487 | let state = self.format_state(); |
| 1478 | 1488 | let statuses: Vec<_> = commands::COMMANDS |
| 1479 | 1489 | .iter() |
| 1480 | .map(|command| { | |
| 1481 | ( | |
| 1482 | command.id, | |
| 1483 | self.status(&Choice::Command(command.id), &state), | |
| 1484 | ) | |
| 1485 | }) | |
| 1490 | .map(|command| command.id) | |
| 1491 | .chain((0..self.tags.len()).map(Cmd::Tag)) | |
| 1492 | .map(|id| (id, self.status(&Choice::Command(id), &state))) | |
| 1486 | 1493 | .collect(); |
| 1487 | 1494 | let status_of = |id| { |
| 1488 | 1495 | statuses |
| ... | ... | @@ -1499,7 +1506,7 @@ impl State { |
| 1499 | 1506 | let label = format!("{:.0}%", self.view.zoom() * 100.0); |
| 1500 | 1507 | let ui = &mut self.ui; |
| 1501 | 1508 | let text = theme.text; |
| 1502 | let tags = NoteTag::ALL.map(Cmd::Tag); | |
| 1509 | let tags: Vec<_> = self.tags.iter().enumerate().collect(); | |
| 1503 | 1510 | let mut choice = group( |
| 1504 | 1511 | ui, |
| 1505 | 1512 | "navigate", |
| ... | ... | @@ -1890,11 +1897,13 @@ impl State { |
| 1890 | 1897 | ) |
| 1891 | 1898 | .or(choice); |
| 1892 | 1899 | // The gallery's menu: every tag, then managing them. |
| 1893 | let mut all_tags: Vec<_> = tags.into_iter().map(Run).collect(); | |
| 1900 | let mut all_tags: Vec<_> = tags | |
| 1901 | .iter() | |
| 1902 | .map(|&(place, tag)| Entry::Tag(place, tag)) | |
| 1903 | .collect(); | |
| 1894 | 1904 | all_tags.extend([ |
| 1895 | 1905 | Rule, |
| 1896 | // Customize Tags awaits #8. | |
| 1897 | Soon("Customize Tags…", None), | |
| 1906 | Run(Cmd::CustomizeTags), | |
| 1898 | 1907 | Run(Cmd::RemoveTags), |
| 1899 | 1908 | Rule, |
| 1900 | 1909 | Run(Cmd::FindTags), |
| ... | ... | @@ -1906,12 +1915,18 @@ impl State { |
| 1906 | 1915 | |ui| { |
| 1907 | 1916 | divider(ui, theme); |
| 1908 | 1917 | let mut choice = None; |
| 1909 | for id in &tags[..3] { | |
| 1910 | choice = tool(ui, *id, status_of(*id)).or(choice); | |
| 1918 | for &(place, tag) in tags.iter().take(3) { | |
| 1919 | choice = tag_tool(ui, place, tag, status_of(Cmd::Tag(place))).or(choice); | |
| 1911 | 1920 | } |
| 1912 | 1921 | dropdown(ui, "tags", Head::More, &all_tags, status_of).or(choice) |
| 1913 | 1922 | }, |
| 1914 | |ui| dropdown(ui, "menu", Head::Split(tags[0]), &all_tags, status_of), | |
| 1923 | |ui| { | |
| 1924 | let head = match tags.first() { | |
| 1925 | Some(&(place, tag)) => Head::Tag(place, tag), | |
| 1926 | None => Head::More, | |
| 1927 | }; | |
| 1928 | dropdown(ui, "menu", head, &all_tags, status_of) | |
| 1929 | }, | |
| 1915 | 1930 | ) |
| 1916 | 1931 | .or(choice); |
| 1917 | 1932 | let inserted = [Cmd::Picture, Cmd::Link, Cmd::Date, Cmd::Equation]; |
| ... | ... | @@ -2342,6 +2357,7 @@ impl State { |
| 2342 | 2357 | self.respond(response); |
| 2343 | 2358 | } |
| 2344 | 2359 | Command::Page(Request::OpenLink(address)) => self.open_link(&address)?, |
| 2360 | Command::Page(Request::OpenAttachment(file)) => self.open_attachment(&file)?, | |
| 2345 | 2361 | Command::Choose(choice) => self.run(choice)?, |
| 2346 | 2362 | } |
| 2347 | 2363 | Ok(()) |
| ... | ... | @@ -3789,8 +3805,10 @@ fn group( |
| 3789 | 3805 | |
| 3790 | 3806 | /// A row of a toolbar menu. |
| 3791 | 3807 | #[derive(Clone, Copy)] |
| 3792 | enum Entry { | |
| 3808 | enum Entry<'a> { | |
| 3793 | 3809 | Run(commands::Id), |
| 3810 | /// Applies the tag at this place in the tag list. | |
| 3811 | Tag(usize, &'a canvas::editor::NoteTag), | |
| 3794 | 3812 | /// Opens the toolbar's popup `name`, which applies the command, under the button the |
| 3795 | 3813 | /// popup's own control would open it from, or where that is folded away, this menu's. |
| 3796 | 3814 | Open(&'static str, commands::Id), |
| ... | ... | @@ -3802,8 +3820,10 @@ enum Entry { |
| 3802 | 3820 | |
| 3803 | 3821 | /// What opens a toolbar menu: the arrow of a button running a command, a button showing |
| 3804 | 3822 | /// artwork, or a bare arrow. |
| 3805 | enum Head { | |
| 3823 | enum Head<'a> { | |
| 3806 | 3824 | Split(commands::Id), |
| 3825 | /// A button applying the tag at this place in the tag list. | |
| 3826 | Tag(usize, &'a canvas::editor::NoteTag), | |
| 3807 | 3827 | Menu(&'static [&'static str]), |
| 3808 | 3828 | More, |
| 3809 | 3829 | } |
| ... | ... | @@ -3831,8 +3851,17 @@ fn dropdown( |
| 3831 | 3851 | tip(ui, id); |
| 3832 | 3852 | ui::Anchor::Below(ui.rect(button).unwrap_or_default()) |
| 3833 | 3853 | } |
| 3854 | Head::Tag(place, tag) if status_of(commands::Id::Tag(place)).enabled => { | |
| 3855 | let on = status_of(commands::Id::Tag(place)).checked; | |
| 3856 | if ui::shell::split_button(ui, part, tag_art(tag), None, on, menu).clicked { | |
| 3857 | choice = Some(Choice::Command(commands::Id::Tag(place))); | |
| 3858 | } | |
| 3859 | tag_tip(ui, place, tag); | |
| 3860 | ui::Anchor::Below(ui.rect(button).unwrap_or_default()) | |
| 3861 | } | |
| 3834 | 3862 | // Where its command does not apply, the button only opens the menu. |
| 3835 | 3863 | Head::Split(id) => ui::shell::menu_button(ui, part, artwork(id).unwrap_or_default(), menu), |
| 3864 | Head::Tag(_, tag) => ui::shell::menu_button(ui, part, tag_art(tag), menu), | |
| 3836 | 3865 | Head::Menu(icon) => ui::shell::menu_button(ui, part, icon, menu), |
| 3837 | 3866 | Head::More => { |
| 3838 | 3867 | let open = ui.popup_open(menu); |
| ... | ... | @@ -3847,6 +3876,7 @@ fn dropdown( |
| 3847 | 3876 | .iter() |
| 3848 | 3877 | .map(|entry| match entry { |
| 3849 | 3878 | Entry::Run(id) => commands::shortcut(*id), |
| 3879 | Entry::Tag(place, _) => commands::shortcut(commands::Id::Tag(*place)), | |
| 3850 | 3880 | _ => String::new(), |
| 3851 | 3881 | }) |
| 3852 | 3882 | .collect(); |
| ... | ... | @@ -3861,12 +3891,22 @@ fn dropdown( |
| 3861 | 3891 | Entry::Run(id) => ui::popup::Item { |
| 3862 | 3892 | text: commands::command(id).title, |
| 3863 | 3893 | icon: artwork(id), |
| 3864 | colored: matches!(id, commands::Id::Tag(_)), | |
| 3865 | 3894 | shortcut: key, |
| 3866 | 3895 | checked: status_of(id).checked, |
| 3867 | 3896 | disabled: !status_of(id).enabled, |
| 3868 | 3897 | ..Default::default() |
| 3869 | 3898 | }, |
| 3899 | Entry::Tag(place, tag) => ui::popup::Item { | |
| 3900 | text: &tag.label, | |
| 3901 | icon: TagIcon::of(tag.shape, false).map(tag_sources), | |
| 3902 | ink: tag.color.map(colorref), | |
| 3903 | highlight: tag.highlight.map(colorref), | |
| 3904 | colored: true, | |
| 3905 | shortcut: key, | |
| 3906 | checked: status_of(commands::Id::Tag(place)).checked, | |
| 3907 | disabled: !status_of(commands::Id::Tag(place)).enabled, | |
| 3908 | ..Default::default() | |
| 3909 | }, | |
| 3870 | 3910 | Entry::Open(_, id) => ui::popup::Item { |
| 3871 | 3911 | text: commands::command(id).title, |
| 3872 | 3912 | icon: artwork(id), |
| ... | ... | @@ -3889,6 +3929,7 @@ fn dropdown( |
| 3889 | 3929 | } |
| 3890 | 3930 | match ui::popup::menu(ui, menu, anchor, &items, None).map(|index| actions[index]) { |
| 3891 | 3931 | Some(Entry::Run(id)) => Some(Choice::Command(id)), |
| 3932 | Some(Entry::Tag(place, _)) => Some(Choice::Command(commands::Id::Tag(place))), | |
| 3892 | 3933 | Some(Entry::Open(name, _)) => { |
| 3893 | 3934 | ui.open_popup(toolbar_popup(name)); |
| 3894 | 3935 | None |
| ... | ... | @@ -3904,12 +3945,7 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma |
| 3904 | 3945 | .iter() |
| 3905 | 3946 | .position(|command| command.id == id); |
| 3906 | 3947 | let icon = artwork(id).unwrap_or_default(); |
| 3907 | // Artwork in its own colours takes white. | |
| 3908 | let tint = if matches!(id, commands::Id::Tag(_)) { | |
| 3909 | [1.0; 4] | |
| 3910 | } else { | |
| 3911 | ui.theme.text | |
| 3912 | }; | |
| 3948 | let tint = ui.theme.text; | |
| 3913 | 3949 | if !status.enabled { |
| 3914 | 3950 | ui::shell::unavailable(ui, part, icon, tint, false); |
| 3915 | 3951 | return None; |
| ... | ... | @@ -3919,6 +3955,25 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma |
| 3919 | 3955 | clicked.then_some(commands::Choice::Command(id)) |
| 3920 | 3956 | } |
| 3921 | 3957 | |
| 3958 | /// A tool button applying the tag at `place` in the tag list, as `tool` builds a command's. | |
| 3959 | fn tag_tool( | |
| 3960 | ui: &mut Ui, | |
| 3961 | place: usize, | |
| 3962 | tag: &canvas::editor::NoteTag, | |
| 3963 | status: commands::Status, | |
| 3964 | ) -> Option<commands::Choice> { | |
| 3965 | let part = ("tag", place); | |
| 3966 | // Artwork in its own colours takes white. | |
| 3967 | let tint = [1.0; 4]; | |
| 3968 | if !status.enabled { | |
| 3969 | ui::shell::unavailable(ui, part, tag_art(tag), tint, false); | |
| 3970 | return None; | |
| 3971 | } | |
| 3972 | let clicked = ui::shell::tool_button(ui, part, tag_art(tag), tint, status.checked).clicked; | |
| 3973 | tag_tip(ui, place, tag); | |
| 3974 | clicked.then_some(commands::Choice::Command(commands::Id::Tag(place))) | |
| 3975 | } | |
| 3976 | ||
| 3922 | 3977 | /// The artwork the toolbar and its menus show for command `id`. |
| 3923 | 3978 | fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { |
| 3924 | 3979 | use canvas::editor::{Alignment, Toggle}; |
| ... | ... | @@ -3948,7 +4003,6 @@ fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { |
| 3948 | 4003 | Cmd::Align(Alignment::Right) => art::ALIGN_RIGHT, |
| 3949 | 4004 | Cmd::Outdent => art::OUTDENT, |
| 3950 | 4005 | Cmd::Indent => art::INDENT, |
| 3951 | Cmd::Tag(tag) => tag_icon(tag), | |
| 3952 | 4006 | Cmd::FindTags => art::FIND_TAGS, |
| 3953 | 4007 | Cmd::Table => art::TABLE, |
| 3954 | 4008 | Cmd::Picture => art::PICTURE, |
| ... | ... | @@ -3970,14 +4024,36 @@ fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { |
| 3970 | 4024 | }) |
| 3971 | 4025 | } |
| 3972 | 4026 | |
| 3973 | /// The artwork the toolbar shows for `tag`. | |
| 3974 | fn tag_icon(tag: canvas::editor::NoteTag) -> &'static [&'static str] { | |
| 3975 | use canvas::editor::NoteTag; | |
| 3976 | match tag { | |
| 3977 | NoteTag::RememberForLater => art::TAG_REMEMBER, | |
| 3978 | NoteTag::Definition => art::TAG_DEFINITION, | |
| 3979 | tag => tag_sources(TagIcon::of(tag.shape(), false).unwrap_or(TagIcon::Other)), | |
| 4027 | /// The artwork the toolbar shows for `tag`: its symbol, or for a tag that only colours | |
| 4028 | /// text, a chip of lines in its font colour on its highlight. | |
| 4029 | fn tag_art(tag: &canvas::editor::NoteTag) -> &'static [&'static str] { | |
| 4030 | use std::collections::BTreeMap; | |
| 4031 | use std::sync::Mutex; | |
| 4032 | // Artwork is borrowed for the program's life, so each colour pair is made once. | |
| 4033 | type Chips = BTreeMap<(Option<u32>, Option<u32>), &'static [&'static str]>; | |
| 4034 | static CHIPS: Mutex<Chips> = Mutex::new(BTreeMap::new()); | |
| 4035 | if let Some(icon) = TagIcon::of(tag.shape, false) { | |
| 4036 | return tag_sources(icon); | |
| 3980 | 4037 | } |
| 4038 | let hex = |colorref: u32| { | |
| 4039 | let [red, green, blue, _] = colorref.to_le_bytes(); | |
| 4040 | format!("#{red:02x}{green:02x}{blue:02x}") | |
| 4041 | }; | |
| 4042 | let mut chips = CHIPS | |
| 4043 | .lock() | |
| 4044 | .unwrap_or_else(|poisoned| poisoned.into_inner()); | |
| 4045 | chips.entry((tag.color, tag.highlight)).or_insert_with(|| { | |
| 4046 | let ink = tag.color.map_or_else(|| "#000000".into(), hex); | |
| 4047 | let fill = tag.highlight.map_or_else(|| "#ffffff".into(), hex); | |
| 4048 | let svg = format!( | |
| 4049 | r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 4050 | <path d="M2 3H14A1 1 0 0 1 15 4V12A1 1 0 0 1 14 13H2A1 1 0 0 1 1 12V4A1 1 0 0 1 2 3Z" fill="#000000" fill-opacity="0.35"/> | |
| 4051 | <path d="M2.25 4H13.75A0.25 0.25 0 0 1 14 4.25V11.75A0.25 0.25 0 0 1 13.75 12H2.25A0.25 0.25 0 0 1 2 11.75V4.25A0.25 0.25 0 0 1 2.25 4Z" fill="{fill}"/> | |
| 4052 | <path d="M3.5 6H12.5V7.25H3.5ZM3.5 8.75H10V10H3.5Z" fill="{ink}"/> | |
| 4053 | </svg>"## | |
| 4054 | ); | |
| 4055 | Box::leak(Box::new([&*svg.leak()])) | |
| 4056 | }) | |
| 3981 | 4057 | } |
| 3982 | 4058 | |
| 3983 | 4059 | /// The toolbar's popup `name`, which the menu bar opens too. |
| ... | ... | @@ -3985,6 +4061,12 @@ fn toolbar_popup(name: &str) -> Id { |
| 3985 | 4061 | Id::ROOT.child(("popup", name)) |
| 3986 | 4062 | } |
| 3987 | 4063 | |
| 4064 | /// A tooltip naming the tag at `place` and its chord on the box built last. | |
| 4065 | fn tag_tip(ui: &mut Ui, place: usize, tag: &canvas::editor::NoteTag) { | |
| 4066 | let key = commands::shortcut(commands::Id::Tag(place)); | |
| 4067 | ui::popup::tooltip(ui, &tag.label, &key, None); | |
| 4068 | } | |
| 4069 | ||
| 3988 | 4070 | /// A tooltip naming command `id` and its chord on the box built last. It names the command, |
| 3989 | 4071 | /// not a dialog it opens, so it leaves off the menu's ellipsis. |
| 3990 | 4072 | fn tip(ui: &mut Ui, id: commands::Id) { |
| ... | ... | @@ -4344,6 +4426,11 @@ impl ApplicationHandler<UserEvent> for App { |
| 4344 | 4426 | }) |
| 4345 | 4427 | } |
| 4346 | 4428 | WindowEvent::Ime(ime) => state.input(ui::Event::Ime(ime)), |
| 4429 | WindowEvent::DroppedFile(path) => { | |
| 4430 | let at = platform::drop_point(&state.window); | |
| 4431 | state.attach(&path, at)?; | |
| 4432 | state.window.request_redraw(); | |
| 4433 | } | |
| 4347 | 4434 | WindowEvent::PinchGesture { delta, .. } => state.pinch(1.0 + delta as f32)?, |
| 4348 | 4435 | // Smart zoom: to 200% about the fingers, or back to 100%. |
| 4349 | 4436 | WindowEvent::DoubleTapGesture { .. } => { |
crates/snowbound/src/manage.rs+81-68| ... | ... | @@ -7,7 +7,7 @@ use notebook::session::Notebook; |
| 7 | 7 | use onestore::{ |
| 8 | 8 | ExGuid, PageCreation, PageEdit, |
| 9 | 9 | op::{Edit, Op, PageOp, SectionOp}, |
| 10 | page::{Image, Page, PageObject}, | |
| 10 | page::{Page, PageObject}, | |
| 11 | 11 | }; |
| 12 | 12 | use std::{error::Error, sync::Arc}; |
| 13 | 13 | |
| ... | ... | @@ -134,26 +134,19 @@ impl State { |
| 134 | 134 | self.edit_page(ops) |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | /// Gives the open page `template`'s art, or none, in place of the art it had; its colour | |
| 138 | /// and rule lines stay. | |
| 139 | pub(crate) fn art_page( | |
| 140 | &mut self, | |
| 141 | template: Option<&'static Template>, | |
| 142 | ) -> Result<(), Box<dyn Error>> { | |
| 143 | self.persist()?; | |
| 144 | let session = self.session.as_ref().ok_or("No section is open")?; | |
| 145 | let ops = art_ops(&session.section.page(session.space)?, template)?; | |
| 146 | self.edit_page(ops) | |
| 147 | } | |
| 148 | ||
| 149 | /// View, Page Color and Rule Lines: gives the open page `color`, a COLORREF, and | |
| 150 | /// `rule_lines` as one undo step; its art stays. | |
| 137 | /// View, Page Color and Rule Lines: gives the open page `color`, a COLORREF, | |
| 138 | /// `rule_lines` and, when given, `template`'s art or none in place of the art it had, as | |
| 139 | /// one undo step. | |
| 151 | 140 | pub(crate) fn paper_page( |
| 152 | 141 | &mut self, |
| 153 | 142 | color: Option<u32>, |
| 154 | 143 | rule_lines: Option<onestore::page::RuleLines>, |
| 144 | art: Option<Option<&Template>>, | |
| 155 | 145 | ) -> Result<(), Box<dyn Error>> { |
| 156 | let response = self.view.set_paper(color, rule_lines)?; | |
| 146 | let art = art | |
| 147 | .map(|template| template.map_or(Ok(Vec::new()), Template::pictures)) | |
| 148 | .transpose()?; | |
| 149 | let response = self.view.set_paper(color, rule_lines, art)?; | |
| 157 | 150 | self.respond(response); |
| 158 | 151 | Ok(()) |
| 159 | 152 | } |
| ... | ... | @@ -177,7 +170,7 @@ impl State { |
| 177 | 170 | |
| 178 | 171 | /// Asks where to create a notebook and creates it, as File, New does in OneNote. |
| 179 | 172 | pub(crate) fn new_notebook(&mut self) -> Result<(), Box<dyn Error>> { |
| 180 | let Some(root) = platform::pick_new("New Notebook", "My Notebook") else { | |
| 173 | let Some(root) = platform::pick_new("New Notebook", "My Notebook", "Create") else { | |
| 181 | 174 | return Ok(()); |
| 182 | 175 | }; |
| 183 | 176 | let page = self.dated_page(None)?; |
| ... | ... | @@ -503,25 +496,9 @@ pub fn art_ops(page: &Page, template: Option<&Template>) -> Result<Vec<PageOp>, |
| 503 | 496 | _ => true, |
| 504 | 497 | }) |
| 505 | 498 | .map(PageObject::id); |
| 506 | for art in template.map_or(&[][..], |template| template.art) { | |
| 507 | let bytes = | |
| 508 | canvas::gpu::page::template_picture(art.art).ok_or("That template's art is missing")?; | |
| 499 | for image in template.map_or(Ok(Vec::new()), Template::pictures)? { | |
| 509 | 500 | ops.push(PageOp::Add { |
| 510 | object: PageObject::Image(Image { | |
| 511 | id: onestore::page::text::new_id()?, | |
| 512 | layout: onestore::document::Layout { | |
| 513 | x: Some(art.position[0]), | |
| 514 | y: Some(art.position[1]), | |
| 515 | max_width: art.size.map(|size| size[0]), | |
| 516 | max_height: art.size.map(|size| size[1]), | |
| 517 | width_set_by_user: art.size.map(|_| true), | |
| 518 | ..Default::default() | |
| 519 | }, | |
| 520 | size: art.size, | |
| 521 | bytes: Some(bytes.into()), | |
| 522 | alt: None, | |
| 523 | background: true, | |
| 524 | }), | |
| 501 | object: PageObject::Image(image), | |
| 525 | 502 | before: under, |
| 526 | 503 | }); |
| 527 | 504 | } |
| ... | ... | @@ -873,9 +850,10 @@ mod tests { |
| 873 | 850 | } |
| 874 | 851 | |
| 875 | 852 | /// Backgrounds the Page Color menu gives OneNote's ruled pages |
| 876 | /// (`corpus/rule-lines/native`): art on a page with rule lines, keeping them, art taken | |
| 877 | /// away, and a colour outside OneNote's menu. `SNOWBOUND_BACKGROUND_EXPORT` names a new | |
| 878 | /// directory that receives the notebook for a cold reopen in OneNote 2010. | |
| 853 | /// (`corpus/rule-lines/native`) through the editor, each step one edit: art on a page | |
| 854 | /// with rule lines, keeping them; art taken away, undone, redone and replaced; a colour | |
| 855 | /// outside OneNote's menu; and art on a coloured page. `SNOWBOUND_BACKGROUND_EXPORT` | |
| 856 | /// names a new directory that receives the notebook for a cold reopen in OneNote 2010. | |
| 879 | 857 | #[test] |
| 880 | 858 | fn backgrounds_go_on_and_off_pages_that_have_content() { |
| 881 | 859 | let temporary = |
| ... | ... | @@ -896,38 +874,73 @@ mod tests { |
| 896 | 874 | .unwrap() |
| 897 | 875 | .0 |
| 898 | 876 | }; |
| 899 | let on_page = |space, ops: Vec<PageOp>| { | |
| 900 | edit( | |
| 901 | &file, | |
| 902 | ops.into_iter().map(|op| Op::Page { space, op }).collect(), | |
| 903 | ) | |
| 877 | enum Step { | |
| 878 | Art(Option<&'static str>), | |
| 879 | Color(u32), | |
| 880 | Undo, | |
| 881 | Redo, | |
| 882 | } | |
| 883 | use Step::*; | |
| 884 | let mut engine = canvas::layout::TextEngine::default(); | |
| 885 | let mut run = |title: &str, steps: &[Step]| { | |
| 886 | let space = titled(title); | |
| 887 | let mut editor = | |
| 888 | canvas::editor::CanvasEditor::from_page(page(&file, space), &mut engine).unwrap(); | |
| 889 | for step in steps { | |
| 890 | let (color, lines) = (editor.page_color(), editor.rule_lines()); | |
| 891 | let changed = match step { | |
| 892 | Art(name) => { | |
| 893 | let art = name.map_or(Ok(Vec::new()), |name| { | |
| 894 | canvas::template::find(name).unwrap().pictures() | |
| 895 | }); | |
| 896 | editor.set_paper(color, lines, Some(art.unwrap())) | |
| 897 | } | |
| 898 | Color(color) => editor.set_paper(Some(*color), lines, None), | |
| 899 | Undo => editor.undo(&mut engine).unwrap(), | |
| 900 | Redo => editor.redo(&mut engine).unwrap(), | |
| 901 | }; | |
| 902 | assert!(changed); | |
| 903 | let ops = editor.take_ops().unwrap(); | |
| 904 | assert!(!ops.is_empty()); | |
| 905 | edit( | |
| 906 | &file, | |
| 907 | ops.into_iter().map(|op| Op::Page { space, op }).collect(), | |
| 908 | ); | |
| 909 | } | |
| 910 | page(&file, space) | |
| 904 | 911 | }; |
| 905 | let ivy = canvas::template::find("Ivy"); | |
| 906 | let (standard, wide, grid) = (titled("Standard"), titled("Wide"), titled("SmallGrid")); | |
| 907 | on_page(standard, art_ops(&page(&file, standard), ivy).unwrap()); | |
| 908 | on_page(wide, art_ops(&page(&file, wide), ivy).unwrap()); | |
| 909 | on_page(wide, art_ops(&page(&file, wide), None).unwrap()); | |
| 910 | // A warm grey no menu offers. | |
| 911 | on_page(grid, vec![PageOp::Color(Some(0x00c8d8e8))]); | |
| 912 | ||
| 913 | let standard = page(&file, standard); | |
| 914 | assert!(standard.rule_lines.is_some()); | |
| 915 | assert_eq!( | |
| 916 | standard | |
| 917 | .objects | |
| 912 | let lemon = canvas::template::PAGE_COLORS[2]; | |
| 913 | assert_eq!(lemon.0, "Lemon"); | |
| 914 | let pages = [ | |
| 915 | run("Standard", &[Art(Some("Ivy"))]), | |
| 916 | run("Wide", &[Art(Some("Ivy")), Art(None)]), | |
| 917 | // A warm grey no menu offers. | |
| 918 | run("SmallGrid", &[Color(0x00c8d8e8)]), | |
| 919 | run("College", &[Art(Some("Ivy")), Undo]), | |
| 920 | run("MediumGrid", &[Art(Some("Ivy")), Undo, Redo]), | |
| 921 | run("LargeGrid", &[Art(Some("Ivy")), Art(Some("Tulips")), Undo]), | |
| 922 | run("VeryLargeGrid", &[Color(lemon.1), Art(Some("Sparks"))]), | |
| 923 | ]; | |
| 924 | let art = |page: &Page| -> Vec<f32> { | |
| 925 | page.objects | |
| 918 | 926 | .iter() |
| 919 | .filter(|object| matches!(object, PageObject::Image(image) if image.background)) | |
| 920 | .count(), | |
| 921 | ivy.unwrap().art.len() | |
| 922 | ); | |
| 923 | let wide = page(&file, wide); | |
| 924 | assert!( | |
| 925 | !wide | |
| 926 | .objects | |
| 927 | .iter() | |
| 928 | .any(|object| matches!(object, PageObject::Image(_))) | |
| 929 | ); | |
| 930 | assert_eq!(page(&file, grid).color, Some(0x00c8d8e8)); | |
| 927 | .filter_map(|object| match object { | |
| 928 | PageObject::Image(image) if image.background => image.layout.max_width, | |
| 929 | _ => None, | |
| 930 | }) | |
| 931 | .collect() | |
| 932 | }; | |
| 933 | let [standard, wide, grid, college, medium, large, very_large] = &pages; | |
| 934 | let [ivy, sparks] = ["Ivy", "Sparks"] | |
| 935 | .map(|name| canvas::template::find(name).unwrap().art[0].size.unwrap()[0]); | |
| 936 | assert_eq!(art(standard), [ivy]); | |
| 937 | assert!(pages.iter().all(|page| page.rule_lines.is_some())); | |
| 938 | assert!(art(wide).is_empty() && art(college).is_empty()); | |
| 939 | assert_eq!(grid.color, Some(0x00c8d8e8)); | |
| 940 | assert_eq!(art(medium), [ivy]); | |
| 941 | assert_eq!(art(large), [ivy]); | |
| 942 | assert_eq!(art(very_large), [sparks]); | |
| 943 | assert_eq!(very_large.color, Some(lemon.1)); | |
| 931 | 944 | if let Some(directory) = std::env::var_os("SNOWBOUND_BACKGROUND_EXPORT") { |
| 932 | 945 | copy(&temporary, Path::new(&directory)); |
| 933 | 946 | } |
crates/snowbound/src/menubar.rs+37-42| ... | ... | @@ -156,48 +156,8 @@ const MENUS: &[Item] = &[ |
| 156 | 156 | C(Id::ClearFormatting), |
| 157 | 157 | ], |
| 158 | 158 | ), |
| 159 | Item::Menu( | |
| 160 | "Tags", | |
| 161 | &[ | |
| 162 | C(Id::Tag(NoteTag::ToDo)), | |
| 163 | C(Id::Tag(NoteTag::Important)), | |
| 164 | C(Id::Tag(NoteTag::Question)), | |
| 165 | C(Id::Tag(NoteTag::RememberForLater)), | |
| 166 | C(Id::Tag(NoteTag::Definition)), | |
| 167 | C(Id::Tag(NoteTag::Highlight)), | |
| 168 | C(Id::Tag(NoteTag::Contact)), | |
| 169 | C(Id::Tag(NoteTag::Address)), | |
| 170 | C(Id::Tag(NoteTag::PhoneNumber)), | |
| 171 | Item::Menu( | |
| 172 | "More Tags", | |
| 173 | &[ | |
| 174 | C(Id::Tag(NoteTag::WebSiteToVisit)), | |
| 175 | C(Id::Tag(NoteTag::Idea)), | |
| 176 | C(Id::Tag(NoteTag::Password)), | |
| 177 | C(Id::Tag(NoteTag::Critical)), | |
| 178 | C(Id::Tag(NoteTag::ProjectA)), | |
| 179 | C(Id::Tag(NoteTag::ProjectB)), | |
| 180 | C(Id::Tag(NoteTag::MovieToSee)), | |
| 181 | C(Id::Tag(NoteTag::BookToRead)), | |
| 182 | C(Id::Tag(NoteTag::MusicToListenTo)), | |
| 183 | C(Id::Tag(NoteTag::SourceForArticle)), | |
| 184 | C(Id::Tag(NoteTag::RememberForBlog)), | |
| 185 | C(Id::Tag(NoteTag::DiscussWithPersonA)), | |
| 186 | C(Id::Tag(NoteTag::DiscussWithPersonB)), | |
| 187 | C(Id::Tag(NoteTag::DiscussWithManager)), | |
| 188 | C(Id::Tag(NoteTag::SendInEmail)), | |
| 189 | C(Id::Tag(NoteTag::ScheduleMeeting)), | |
| 190 | C(Id::Tag(NoteTag::CallBack)), | |
| 191 | C(Id::Tag(NoteTag::ToDoPriority1)), | |
| 192 | C(Id::Tag(NoteTag::ToDoPriority2)), | |
| 193 | C(Id::Tag(NoteTag::ClientRequest)), | |
| 194 | ], | |
| 195 | ), | |
| 196 | S, | |
| 197 | C(Id::RemoveTags), | |
| 198 | C(Id::FindTags), | |
| 199 | ], | |
| 200 | ), | |
| 159 | // Filled from the tag list by `tags`. | |
| 160 | Item::Menu("Tags", &[]), | |
| 201 | 161 | Item::Menu( |
| 202 | 162 | "Window", |
| 203 | 163 | &[ |
| ... | ... | @@ -216,6 +176,7 @@ pub struct Menus { |
| 216 | 176 | pub services: Retained<NSMenu>, |
| 217 | 177 | pub window: Retained<NSMenu>, |
| 218 | 178 | pub help: Retained<NSMenu>, |
| 179 | pub tags: Retained<NSMenu>, | |
| 219 | 180 | } |
| 220 | 181 | |
| 221 | 182 | /// Builds the bar; the table's items send `choose:` to `target`, which validates them. |
| ... | ... | @@ -241,6 +202,7 @@ pub fn build(mtm: MainThreadMarker, target: &AnyObject) -> Menus { |
| 241 | 202 | .expect("The bar has the menus AppKit fills") |
| 242 | 203 | }; |
| 243 | 204 | Menus { |
| 205 | tags: named("Tags"), | |
| 244 | 206 | window: named("Window"), |
| 245 | 207 | help: named("Help"), |
| 246 | 208 | services, |
| ... | ... | @@ -302,6 +264,38 @@ fn menu( |
| 302 | 264 | menu |
| 303 | 265 | } |
| 304 | 266 | |
| 267 | /// Fills the Tags menu from the user's tag list, as OneNote's gallery lists it: the first | |
| 268 | /// nine with their chords, the rest under More Tags, then managing them. | |
| 269 | pub fn tags(mtm: MainThreadMarker, target: &AnyObject, menu: &NSMenu, tags: &[NoteTag]) { | |
| 270 | unsafe { menu.removeAllItems() }; | |
| 271 | let choose = |menu: &NSMenu, id: Id, title: &str, chord: Option<Chord>| { | |
| 272 | let item = entry(mtm, title, Some(objc2::sel!(choose:)), chord); | |
| 273 | unsafe { | |
| 274 | item.setTarget(Some(target)); | |
| 275 | item.setTag(commands::index(&Choice::Command(id)) as isize); | |
| 276 | } | |
| 277 | menu.addItem(&item); | |
| 278 | }; | |
| 279 | let more = unsafe { NSMenu::initWithTitle(mtm.alloc(), &NSString::from_str("More Tags")) }; | |
| 280 | for (place, tag) in tags.iter().enumerate().take(commands::TAGS) { | |
| 281 | let chord = commands::tag_chord(place); | |
| 282 | let shown = if chord.is_some() { menu } else { &*more }; | |
| 283 | choose(shown, Id::Tag(place), &tag.label, chord); | |
| 284 | } | |
| 285 | if tags.len() > 9 { | |
| 286 | let holder = entry(mtm, "More Tags", None, None); | |
| 287 | holder.setSubmenu(Some(&more)); | |
| 288 | menu.addItem(&holder); | |
| 289 | } | |
| 290 | if !tags.is_empty() { | |
| 291 | menu.addItem(&NSMenuItem::separatorItem(mtm)); | |
| 292 | } | |
| 293 | for id in [Id::CustomizeTags, Id::RemoveTags, Id::FindTags] { | |
| 294 | let command = commands::command(id); | |
| 295 | choose(menu, id, command.title, command.mac.first().copied()); | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 305 | 299 | fn entry( |
| 306 | 300 | mtm: MainThreadMarker, |
| 307 | 301 | title: &str, |
| ... | ... | @@ -418,6 +412,7 @@ mod tests { |
| 418 | 412 | let target: Retained<AnyObject> = |
| 419 | 413 | unsafe { objc2::msg_send_id![objc2::class!(NSObject), new] }; |
| 420 | 414 | let menus = build(mtm, &target); |
| 415 | tags(mtm, &target, &menus.tags, &NoteTag::defaults()); | |
| 421 | 416 | let (mut tree, mut chords) = (String::new(), Vec::new()); |
| 422 | 417 | dump(&menus.bar, 0, &mut tree, &mut chords); |
| 423 | 418 | println!("{tree}"); |
crates/snowbound/src/palette.rs+9| ... | ... | @@ -44,6 +44,15 @@ impl State { |
| 44 | 44 | target: Some(Target::Command(command.id)), |
| 45 | 45 | }) |
| 46 | 46 | .collect(); |
| 47 | rows.extend(self.tags.iter().enumerate().map(|(place, tag)| { | |
| 48 | let id = commands::Id::Tag(place); | |
| 49 | Row { | |
| 50 | text: tag.label.clone(), | |
| 51 | after: commands::shortcut(id), | |
| 52 | disabled: !self.status(&Choice::Command(id), &format).enabled, | |
| 53 | target: Some(Target::Command(id)), | |
| 54 | } | |
| 55 | })); | |
| 47 | 56 | let heading = |text: &str| Row { |
| 48 | 57 | text: text.to_owned(), |
| 49 | 58 | after: String::new(), |
crates/snowbound/src/settings.rs+9| ... | ... | @@ -24,6 +24,8 @@ pub struct Settings { |
| 24 | 24 | pub light_pages: bool, |
| 25 | 25 | /// Where searches look first, as "Set This Scope as Default" chose. |
| 26 | 26 | pub search_scope: crate::search::Scope, |
| 27 | /// The tag list Customize Tags edits; none keeps OneNote's. | |
| 28 | pub tags: Option<Vec<canvas::editor::NoteTag>>, | |
| 27 | 29 | } |
| 28 | 30 | |
| 29 | 31 | /// What the toolbar's buttons apply from their menus' last picks. |
| ... | ... | @@ -128,6 +130,7 @@ impl crate::State { |
| 128 | 130 | color_scheme: self.color_scheme, |
| 129 | 131 | light_pages: self.light_pages, |
| 130 | 132 | search_scope: self.search.default, |
| 133 | tags: (self.tags != canvas::editor::NoteTag::defaults()).then(|| self.tags.clone()), | |
| 131 | 134 | }; |
| 132 | 135 | if let Err(error) = settings.save(path) { |
| 133 | 136 | eprintln!("Cannot save the settings in {}: {error}", path.display()); |
| ... | ... | @@ -177,6 +180,12 @@ mod tests { |
| 177 | 180 | color_scheme: ColorScheme::Dark, |
| 178 | 181 | light_pages: true, |
| 179 | 182 | search_scope: crate::search::Scope::Notebook, |
| 183 | tags: Some(vec![canvas::editor::NoteTag { | |
| 184 | label: "Snow check".into(), | |
| 185 | shape: 61, | |
| 186 | color: Some(0x0000_0080), | |
| 187 | highlight: Some(0x00ff_cc00), | |
| 188 | }]), | |
| 180 | 189 | }; |
| 181 | 190 | settings.save(&path).unwrap(); |
| 182 | 191 | assert_eq!(Settings::load(&path), settings); |
crates/snowbound/src/sync.rs+10-29| ... | ... | @@ -2,8 +2,8 @@ |
| 2 | 2 | //! Synchronization for the open section's notebook and each of its sections. |
| 3 | 3 | |
| 4 | 4 | use crate::{Session, State, art, filetime, library, platform}; |
| 5 | use notebook::session::SyncStatus; | |
| 6 | use std::{error::Error, io::ErrorKind}; | |
| 5 | use notebook::session::{SyncState, SyncStatus}; | |
| 6 | use std::error::Error; | |
| 7 | 7 | use ui::{ |
| 8 | 8 | Anchor, Axis, Flags, Id, Overflow, Spec, Theme, Ui, children, fill, fit, px, shell::TOOL, |
| 9 | 9 | }; |
| ... | ... | @@ -20,7 +20,6 @@ fn button() -> Id { |
| 20 | 20 | |
| 21 | 21 | /// The status's label and icon, and what the reader can do about an error. |
| 22 | 22 | fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option<&'static str>) { |
| 23 | use ErrorKind::*; | |
| 24 | 23 | if library::offline() { |
| 25 | 24 | return ( |
| 26 | 25 | "Working offline", |
| ... | ... | @@ -28,48 +27,30 @@ fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option |
| 28 | 27 | Some("Changes stay on this computer until you sync."), |
| 29 | 28 | ); |
| 30 | 29 | } |
| 31 | match sync.error.as_ref().map(std::io::Error::kind) { | |
| 32 | Some(PermissionDenied | ReadOnlyFilesystem) => ( | |
| 30 | match sync.state() { | |
| 31 | SyncState::ReadOnly => ( | |
| 33 | 32 | "Unable to sync", |
| 34 | 33 | art::SYNC_ERROR, |
| 35 | 34 | Some( |
| 36 | 35 | "You can’t change this notebook where it’s stored. Changes stay on this computer.", |
| 37 | 36 | ), |
| 38 | 37 | ), |
| 39 | Some(WouldBlock | ResourceBusy) => ( | |
| 38 | SyncState::InUse => ( | |
| 40 | 39 | "Section in use", |
| 41 | 40 | art::SYNC_BUSY, |
| 42 | 41 | Some("Someone else is saving this section. Sync continues when they finish."), |
| 43 | 42 | ), |
| 44 | Some( | |
| 45 | NotFound | ConnectionRefused | ConnectionReset | ConnectionAborted | NotConnected | |
| 46 | | TimedOut | HostUnreachable | NetworkUnreachable | NetworkDown | BrokenPipe | |
| 47 | | AddrNotAvailable, | |
| 48 | ) => ( | |
| 43 | SyncState::NotConnected => ( | |
| 49 | 44 | "Not connected", |
| 50 | 45 | art::SYNC_OFFLINE, |
| 51 | 46 | Some("Changes stay on this computer and sync when the notebook is back."), |
| 52 | 47 | ), |
| 53 | Some(_) => ("Unable to sync", art::SYNC_ERROR, None), | |
| 54 | None if sync.synced.is_none() || sync.queued > 0 => ("Syncing…", art::SYNC_BUSY, None), | |
| 55 | None => ("Up to date", art::SYNC_DONE, None), | |
| 48 | SyncState::Failed => ("Unable to sync", art::SYNC_ERROR, None), | |
| 49 | SyncState::Syncing => ("Syncing…", art::SYNC_BUSY, None), | |
| 50 | SyncState::UpToDate => ("Up to date", art::SYNC_DONE, None), | |
| 56 | 51 | } |
| 57 | 52 | } |
| 58 | 53 | |
| 59 | /// Labels from the best state to the worst, which a notebook's status shows. | |
| 60 | const ORDER: [&str; 5] = [ | |
| 61 | "Up to date", | |
| 62 | "Syncing…", | |
| 63 | "Section in use", | |
| 64 | "Not connected", | |
| 65 | "Unable to sync", | |
| 66 | ]; | |
| 67 | ||
| 68 | fn rank(sync: &SyncStatus) -> usize { | |
| 69 | let label = describe(sync).0; | |
| 70 | ORDER.iter().position(|shown| *shown == label).unwrap_or(0) | |
| 71 | } | |
| 72 | ||
| 73 | 54 | /// Every section of the open section's notebook with its status, in catalog order: the |
| 74 | 55 | /// open one's from its session, the others' from the notebook's background sync. |
| 75 | 56 | fn sections(session: &Session) -> Vec<(String, SyncStatus)> { |
| ... | ... | @@ -102,7 +83,7 @@ fn overall(sections: &[(String, SyncStatus)]) -> SyncStatus { |
| 102 | 83 | .iter() |
| 103 | 84 | .map(|(_, sync)| sync) |
| 104 | 85 | .filter(|sync| sync.error.is_some()) |
| 105 | .max_by_key(|sync| rank(sync)); | |
| 86 | .max_by_key(|sync| sync.state()); | |
| 106 | 87 | SyncStatus { |
| 107 | 88 | synced: sections |
| 108 | 89 | .iter() |
crates/snowbound/src/tags.rs created+810| ... | ... | @@ -0,0 +1,810 @@ |
| 1 | //! OneNote 2010's Customize Tags dialog and its New Tag and Modify Tag dialog, which edit | |
| 2 | //! the user's tag list: the tags the toolbar, the menus and Ctrl+1 to Ctrl+9 apply. | |
| 3 | ||
| 4 | use crate::{State, commands, platform}; | |
| 5 | use canvas::editor::NoteTag; | |
| 6 | use canvas::gpu::{colorref, tag_sources}; | |
| 7 | use canvas::outline::{TagIcon, symbol_name}; | |
| 8 | use draw::edit::Platform; | |
| 9 | use ui::{Anchor, Axis, Flags, Id, Spec, Ui, children, fill, fit, px}; | |
| 10 | use winit::keyboard::NamedKey; | |
| 11 | ||
| 12 | const WIDTH: f32 = 300.0; | |
| 13 | const EDITOR_WIDTH: f32 = 280.0; | |
| 14 | const ROW: f32 = 20.0; | |
| 15 | /// Rows the list shows at once; the wheel scrolls the rest. | |
| 16 | const ROWS: f32 = 19.0; | |
| 17 | const SIDE: f32 = 16.0; | |
| 18 | const CELL: f32 = 23.0; | |
| 19 | ||
| 20 | /// OneNote 2010's symbol gallery: ten rows of five groups of three, 0 for None, which spans | |
| 21 | /// the first group's top row, and for the empty cells at the end. | |
| 22 | const GALLERY: [[u16; 15]; 10] = [ | |
| 23 | [ | |
| 24 | 0, 0, 0, 40, 13, 61, 41, 82, 62, 106, 107, 108, 124, 125, 126, | |
| 25 | ], | |
| 26 | [3, 2, 1, 43, 84, 64, 34, 75, 54, 109, 18, 110, 127, 128, 27], | |
| 27 | [6, 5, 4, 36, 77, 56, 46, 87, 67, 14, 15, 17, 129, 130, 131], | |
| 28 | [9, 8, 7, 42, 83, 63, 47, 88, 68, 21, 24, 111, 132, 133, 134], | |
| 29 | [ | |
| 30 | 12, 11, 10, 39, 81, 60, 38, 79, 58, 112, 22, 23, 135, 136, 137, | |
| 31 | ], | |
| 32 | [ | |
| 33 | 28, 69, 48, 29, 70, 49, 16, 80, 59, 113, 25, 26, 138, 139, 140, | |
| 34 | ], | |
| 35 | [ | |
| 36 | 30, 71, 50, 31, 72, 51, 37, 78, 57, 114, 115, 116, 141, 142, 143, | |
| 37 | ], | |
| 38 | [32, 73, 52, 33, 74, 53, 45, 86, 66, 117, 118, 119, 0, 0, 0], | |
| 39 | [94, 95, 96, 35, 76, 55, 100, 101, 102, 120, 19, 20, 0, 0, 0], | |
| 40 | [ | |
| 41 | 97, 98, 99, 44, 85, 65, 103, 104, 105, 121, 122, 123, 0, 0, 0, | |
| 42 | ], | |
| 43 | ]; | |
| 44 | ||
| 45 | /// Font Color's swatches, Office's forty in their grid, as COLORREFs. | |
| 46 | const FONT_COLORS: [u32; 40] = [ | |
| 47 | 0x000000, 0x003399, 0x003333, 0x003300, 0x663300, 0x800000, 0x993333, 0x333333, // | |
| 48 | 0x000080, 0x0066ff, 0x008080, 0x008000, 0x808000, 0xff0000, 0x996666, 0x808080, // | |
| 49 | 0x0000ff, 0x0099ff, 0x00cc99, 0x669933, 0xcccc33, 0xff6633, 0x800080, 0x969696, // | |
| 50 | 0xff00ff, 0x00ccff, 0x00ffff, 0x00ff00, 0xffff00, 0xffcc00, 0x663399, 0xc0c0c0, // | |
| 51 | 0xcc99ff, 0x99ccff, 0x99ffff, 0xccffcc, 0xffffcc, 0xffcc99, 0xff99cc, 0xffffff, | |
| 52 | ]; | |
| 53 | ||
| 54 | /// Highlight Color's swatches as COLORREFs, which differ from the highlighter's in Sky Blue | |
| 55 | /// and Dark Green. | |
| 56 | const HIGHLIGHTS: [u32; 15] = [ | |
| 57 | 0x00ffff, 0x00ff00, 0xffcc00, 0xff00ff, 0xff0000, // | |
| 58 | 0x0000ff, 0x800000, 0x808000, 0x003300, 0x800080, // | |
| 59 | 0x000080, 0x008080, 0x808080, 0xc0c0c0, 0x000000, | |
| 60 | ]; | |
| 61 | ||
| 62 | /// The Customize Tags dialog's list while it is open. | |
| 63 | pub struct TagList { | |
| 64 | tags: Vec<NoteTag>, | |
| 65 | selected: Option<usize>, | |
| 66 | /// The New Tag or Modify Tag dialog's fields, and the place Modify Tag replaces. | |
| 67 | editor: Option<(NoteTag, Option<usize>)>, | |
| 68 | } | |
| 69 | ||
| 70 | fn id() -> Id { | |
| 71 | Id::ROOT.child("customize-tags") | |
| 72 | } | |
| 73 | ||
| 74 | fn editor_id() -> Id { | |
| 75 | id().child("tag") | |
| 76 | } | |
| 77 | ||
| 78 | fn name_field() -> Id { | |
| 79 | editor_id().child("name") | |
| 80 | } | |
| 81 | ||
| 82 | fn popup(name: &str) -> Id { | |
| 83 | editor_id().child(("popup", name)) | |
| 84 | } | |
| 85 | ||
| 86 | /// A tag's artwork before its name: its symbol, or none. | |
| 87 | fn artwork(tag: &NoteTag) -> Option<&'static [&'static str]> { | |
| 88 | TagIcon::of(tag.shape, false).map(tag_sources) | |
| 89 | } | |
| 90 | ||
| 91 | /// The tag's name in its colours, as the list and Preview show it. | |
| 92 | fn label(ui: &mut Ui, part: &str, text: &str, tag: &NoteTag) { | |
| 93 | ui.leaf( | |
| 94 | part, | |
| 95 | Spec { | |
| 96 | size: [fit(), px(ROW - 2.0)], | |
| 97 | text: Some(text), | |
| 98 | color: Some(tag.color.map_or(ui.theme.text, colorref)), | |
| 99 | fill: tag.highlight.map(colorref), | |
| 100 | pad: [2.0, 0.0], | |
| 101 | ..Spec::default() | |
| 102 | }, | |
| 103 | ); | |
| 104 | } | |
| 105 | ||
| 106 | fn icon(ui: &mut Ui, part: &str, art: Option<&'static [&'static str]>) { | |
| 107 | ui.leaf( | |
| 108 | part, | |
| 109 | Spec { | |
| 110 | size: [px(SIDE + 4.0), px(ROW)], | |
| 111 | icon: art, | |
| 112 | color: Some([1.0; 4]), | |
| 113 | center: true, | |
| 114 | ..Spec::default() | |
| 115 | }, | |
| 116 | ); | |
| 117 | } | |
| 118 | ||
| 119 | /// A text button that takes no clicks while not `enabled`. | |
| 120 | fn button(ui: &mut Ui, part: &str, text: &str, enabled: bool) -> bool { | |
| 121 | if enabled { | |
| 122 | return ui::button(ui, part, text).clicked; | |
| 123 | } | |
| 124 | let theme = ui.theme.clone(); | |
| 125 | ui.leaf( | |
| 126 | part, | |
| 127 | Spec { | |
| 128 | size: [fit(), px(theme.font_size * 2.0)], | |
| 129 | text: Some(text), | |
| 130 | color: Some(theme.text_dim), | |
| 131 | fill: Some(theme.chip), | |
| 132 | radius: 4.0, | |
| 133 | pad: [theme.font_size * 0.75, 0.0], | |
| 134 | center: true, | |
| 135 | ..Spec::default() | |
| 136 | }, | |
| 137 | ); | |
| 138 | false | |
| 139 | } | |
| 140 | ||
| 141 | /// A square button showing `art` that takes no clicks while not `enabled`, named by a | |
| 142 | /// tooltip. | |
| 143 | fn tool(ui: &mut Ui, part: &str, art: &'static [&'static str], tip: &str, enabled: bool) -> bool { | |
| 144 | let text = ui.theme.text; | |
| 145 | if !enabled { | |
| 146 | ui::shell::unavailable(ui, part, art, text, false); | |
| 147 | return false; | |
| 148 | } | |
| 149 | let clicked = ui::shell::tool_button(ui, part, art, text, false).clicked; | |
| 150 | ui::popup::tooltip(ui, tip, "", None); | |
| 151 | clicked | |
| 152 | } | |
| 153 | ||
| 154 | /// A button showing `art` in its own colours, with `bar` under it, and a menu arrow; opens | |
| 155 | /// popup `menu` and returns where it opens. | |
| 156 | fn picker( | |
| 157 | ui: &mut Ui, | |
| 158 | part: &str, | |
| 159 | art: Option<&'static [&'static str]>, | |
| 160 | bar: Option<[f32; 4]>, | |
| 161 | menu: Id, | |
| 162 | tint: [f32; 4], | |
| 163 | ) -> Anchor { | |
| 164 | let theme = ui.theme.clone(); | |
| 165 | let open = ui.popup_open(menu); | |
| 166 | let button = ui.open( | |
| 167 | part, | |
| 168 | Spec { | |
| 169 | flags: Flags::CLICKABLE, | |
| 170 | size: [children(), px(ui::shell::TOOL)], | |
| 171 | fill: open.then(|| theme.hover()), | |
| 172 | hover_fill: Some(theme.hover()), | |
| 173 | border: Some(theme.chip), | |
| 174 | radius: 4.0, | |
| 175 | ..Spec::default() | |
| 176 | }, | |
| 177 | ); | |
| 178 | ui.open( | |
| 179 | "icon", | |
| 180 | Spec { | |
| 181 | size: [px(ui::shell::TOOL), px(ui::shell::TOOL)], | |
| 182 | icon: art, | |
| 183 | color: Some(tint), | |
| 184 | center: true, | |
| 185 | ..Spec::default() | |
| 186 | }, | |
| 187 | ); | |
| 188 | if let Some(color) = bar { | |
| 189 | let top = (ui::shell::TOOL - 16.0) / 2.0; | |
| 190 | ui.mark([top, top + 13.0, top + 16.0, top + 16.0], color, 0.0); | |
| 191 | } | |
| 192 | ui.close(); | |
| 193 | ui.leaf( | |
| 194 | "arrow", | |
| 195 | Spec { | |
| 196 | size: [px(12.0), px(ui::shell::TOOL)], | |
| 197 | icon: Some(ui::shell::CHEVRON), | |
| 198 | color: Some(theme.text_dim), | |
| 199 | center: true, | |
| 200 | ..Spec::default() | |
| 201 | }, | |
| 202 | ); | |
| 203 | ui.close(); | |
| 204 | if ui.signal(button).pressed { | |
| 205 | ui.open_popup(menu); | |
| 206 | } | |
| 207 | Anchor::Below(ui.rect(button).unwrap_or_default()) | |
| 208 | } | |
| 209 | ||
| 210 | /// The trailing row of a dialog: its buttons at the right. | |
| 211 | fn buttons(ui: &mut Ui) { | |
| 212 | ui.open( | |
| 213 | "buttons", | |
| 214 | Spec { | |
| 215 | size: [fill(), children()], | |
| 216 | pad: [0.0, 8.0], | |
| 217 | gap: 8.0, | |
| 218 | ..Spec::default() | |
| 219 | }, | |
| 220 | ); | |
| 221 | ui.leaf( | |
| 222 | "space", | |
| 223 | Spec { | |
| 224 | size: [fill(), px(1.0)], | |
| 225 | ..Spec::default() | |
| 226 | }, | |
| 227 | ); | |
| 228 | } | |
| 229 | ||
| 230 | /// The frame of a dialog titled `title`, centred in the window. | |
| 231 | fn dialog(ui: &mut Ui, id: Id, title: &str, width: f32) { | |
| 232 | let theme = ui.theme.clone(); | |
| 233 | ui.open_as( | |
| 234 | id, | |
| 235 | Spec { | |
| 236 | axis: Axis::Y, | |
| 237 | size: [px(width), children()], | |
| 238 | fill: Some(theme.popup), | |
| 239 | border: Some(theme.chip), | |
| 240 | shadow: Some(theme.shadow), | |
| 241 | radius: 8.0, | |
| 242 | pad: [14.0, 10.0], | |
| 243 | gap: 6.0, | |
| 244 | anchor: Some(Anchor::Dialog), | |
| 245 | ..Spec::default() | |
| 246 | }, | |
| 247 | ); | |
| 248 | ui.leaf( | |
| 249 | "title", | |
| 250 | Spec { | |
| 251 | size: [fill(), px(ROW + 4.0)], | |
| 252 | text: Some(title), | |
| 253 | bold: true, | |
| 254 | ..Spec::default() | |
| 255 | }, | |
| 256 | ); | |
| 257 | } | |
| 258 | ||
| 259 | impl State { | |
| 260 | /// Customize Tags opens on the tag list with its first tag selected, as OneNote's does. | |
| 261 | pub(crate) fn open_customize_tags(&mut self) { | |
| 262 | self.tag_list = Some(TagList { | |
| 263 | tags: self.tags.clone(), | |
| 264 | selected: (!self.tags.is_empty()).then_some(0), | |
| 265 | editor: None, | |
| 266 | }); | |
| 267 | self.ui.open_popup(id()); | |
| 268 | } | |
| 269 | ||
| 270 | /// Builds the Customize Tags dialog while it is open. OK keeps a changed list, which the | |
| 271 | /// toolbar, the menus and the chords then apply; Cancel, Escape or a press outside leave | |
| 272 | /// it. | |
| 273 | pub(crate) fn customize_tags(&mut self) { | |
| 274 | let Some(list) = &mut self.tag_list else { | |
| 275 | return; | |
| 276 | }; | |
| 277 | let ui = &mut self.ui; | |
| 278 | if !ui.popup_open(id()) { | |
| 279 | self.tag_list = None; | |
| 280 | return; | |
| 281 | } | |
| 282 | let theme = ui.theme.clone(); | |
| 283 | dialog(ui, id(), "Customize Tags", WIDTH); | |
| 284 | ui.leaf( | |
| 285 | "all", | |
| 286 | Spec { | |
| 287 | size: [fill(), px(ROW)], | |
| 288 | text: Some("All Tags:"), | |
| 289 | ..Spec::default() | |
| 290 | }, | |
| 291 | ); | |
| 292 | ui.open( | |
| 293 | "body", | |
| 294 | Spec { | |
| 295 | size: [fill(), children()], | |
| 296 | gap: 6.0, | |
| 297 | ..Spec::default() | |
| 298 | }, | |
| 299 | ); | |
| 300 | ui.open( | |
| 301 | "list", | |
| 302 | Spec { | |
| 303 | flags: Flags::SCROLL | Flags::CLIP, | |
| 304 | axis: Axis::Y, | |
| 305 | size: [fill(), px(ROW * ROWS + 4.0)], | |
| 306 | fill: Some(theme.base), | |
| 307 | border: Some(theme.chip), | |
| 308 | radius: 4.0, | |
| 309 | pad: [2.0, 2.0], | |
| 310 | ..Spec::default() | |
| 311 | }, | |
| 312 | ); | |
| 313 | let mut modify = None; | |
| 314 | for (place, tag) in list.tags.iter().enumerate() { | |
| 315 | let selected = list.selected == Some(place); | |
| 316 | let row = ui.open( | |
| 317 | ("tag", place), | |
| 318 | Spec { | |
| 319 | flags: Flags::CLICKABLE, | |
| 320 | size: [fill(), px(ROW)], | |
| 321 | fill: selected.then_some(theme.selection), | |
| 322 | hover_fill: Some(theme.hover()), | |
| 323 | radius: 3.0, | |
| 324 | ..Spec::default() | |
| 325 | }, | |
| 326 | ); | |
| 327 | icon(ui, "icon", artwork(tag)); | |
| 328 | let text = match commands::tag_chord(place) { | |
| 329 | Some(chord) => format!("{} ({})", tag.label, chord.label(Platform::CURRENT)), | |
| 330 | None => tag.label.clone(), | |
| 331 | }; | |
| 332 | label(ui, "label", &text, tag); | |
| 333 | ui.close(); | |
| 334 | let signal = ui.signal(row); | |
| 335 | if signal.pressed { | |
| 336 | list.selected = Some(place); | |
| 337 | } | |
| 338 | if signal.pressed && signal.unit == draw::edit::SelectionUnit::Word { | |
| 339 | modify = Some(place); | |
| 340 | } | |
| 341 | } | |
| 342 | ui.close(); | |
| 343 | ui.open( | |
| 344 | "order", | |
| 345 | Spec { | |
| 346 | axis: Axis::Y, | |
| 347 | size: [children(), children()], | |
| 348 | gap: 4.0, | |
| 349 | ..Spec::default() | |
| 350 | }, | |
| 351 | ); | |
| 352 | let last = list.tags.len().saturating_sub(1); | |
| 353 | let up = tool( | |
| 354 | ui, | |
| 355 | "up", | |
| 356 | crate::art::CHEVRON_UP, | |
| 357 | "Move Tag Up", | |
| 358 | list.selected.is_some_and(|place| place > 0), | |
| 359 | ); | |
| 360 | let down = tool( | |
| 361 | ui, | |
| 362 | "down", | |
| 363 | ui::shell::CHEVRON, | |
| 364 | "Move Tag Down", | |
| 365 | list.selected.is_some_and(|place| place < last), | |
| 366 | ); | |
| 367 | let remove = tool( | |
| 368 | ui, | |
| 369 | "remove", | |
| 370 | crate::art::CLOSE, | |
| 371 | "Remove", | |
| 372 | list.selected.is_some(), | |
| 373 | ); | |
| 374 | ui.close(); | |
| 375 | ui.close(); | |
| 376 | ui.open( | |
| 377 | "edit", | |
| 378 | Spec { | |
| 379 | size: [fill(), children()], | |
| 380 | gap: 8.0, | |
| 381 | ..Spec::default() | |
| 382 | }, | |
| 383 | ); | |
| 384 | let new = button( | |
| 385 | ui, | |
| 386 | "new", | |
| 387 | "New Tag…", | |
| 388 | list.tags.len() < commands::TAGS && list.editor.is_none(), | |
| 389 | ); | |
| 390 | modify = modify.or_else(|| { | |
| 391 | button(ui, "modify", "Modify Tag…", list.selected.is_some()) | |
| 392 | .then_some(list.selected) | |
| 393 | .flatten() | |
| 394 | }); | |
| 395 | ui.close(); | |
| 396 | buttons(ui); | |
| 397 | let ok = button(ui, "ok", "OK", list.tags != self.tags); | |
| 398 | let cancel = ui::button(ui, "cancel", "Cancel").clicked; | |
| 399 | ui.close(); | |
| 400 | if let Some(place) = list.selected { | |
| 401 | if up { | |
| 402 | list.tags.swap(place, place - 1); | |
| 403 | list.selected = Some(place - 1); | |
| 404 | } else if down { | |
| 405 | list.tags.swap(place, place + 1); | |
| 406 | list.selected = Some(place + 1); | |
| 407 | } else if remove { | |
| 408 | list.tags.remove(place); | |
| 409 | list.selected = (!list.tags.is_empty()).then(|| place.min(list.tags.len() - 1)); | |
| 410 | } | |
| 411 | } | |
| 412 | if new { | |
| 413 | // A new tag has no symbol, and takes the top of the list. | |
| 414 | let tag = NoteTag { | |
| 415 | label: "Undefined".into(), | |
| 416 | shape: 0, | |
| 417 | color: None, | |
| 418 | highlight: None, | |
| 419 | }; | |
| 420 | list.editor = Some((tag, None)); | |
| 421 | } else if let Some(place) = modify { | |
| 422 | list.editor = Some((list.tags[place].clone(), Some(place))); | |
| 423 | } | |
| 424 | if (new || modify.is_some()) && !ui.popup_open(editor_id()) { | |
| 425 | ui.open_popup(editor_id()); | |
| 426 | ui.set_focus(Some(name_field())); | |
| 427 | } | |
| 428 | if let Some((tag, place)) = tag_editor(ui, &mut list.editor) { | |
| 429 | match place { | |
| 430 | Some(place) => list.tags[place] = tag, | |
| 431 | None => { | |
| 432 | list.tags.insert(0, tag); | |
| 433 | list.selected = Some(0); | |
| 434 | } | |
| 435 | } | |
| 436 | } | |
| 437 | ui.close(); | |
| 438 | if ok { | |
| 439 | self.tags = list.tags.clone(); | |
| 440 | self.save_settings(); | |
| 441 | platform::update_tag_menu(&self.tags); | |
| 442 | } else if !cancel { | |
| 443 | return; | |
| 444 | } | |
| 445 | self.ui.close_popup(id()); | |
| 446 | self.ui.set_focus(Some(crate::page())); | |
| 447 | self.tag_list = None; | |
| 448 | } | |
| 449 | } | |
| 450 | ||
| 451 | /// Builds the New Tag or Modify Tag dialog while it is open over Customize Tags. Returns the | |
| 452 | /// tag and the place it replaces once OK keeps it. | |
| 453 | fn tag_editor( | |
| 454 | ui: &mut Ui, | |
| 455 | editor: &mut Option<(NoteTag, Option<usize>)>, | |
| 456 | ) -> Option<(NoteTag, Option<usize>)> { | |
| 457 | if !ui.popup_open(editor_id()) { | |
| 458 | *editor = None; | |
| 459 | return None; | |
| 460 | } | |
| 461 | let (tag, place) = editor.as_mut()?; | |
| 462 | let theme = ui.theme.clone(); | |
| 463 | let entered = | |
| 464 | ui::popup::navigation(ui, &[name_field()], &[NamedKey::Enter]).contains(&NamedKey::Enter); | |
| 465 | let title = if place.is_some() { | |
| 466 | "Modify Tag" | |
| 467 | } else { | |
| 468 | "New Tag" | |
| 469 | }; | |
| 470 | dialog(ui, editor_id(), title, EDITOR_WIDTH); | |
| 471 | let caption = |ui: &mut Ui, part: &str, text: &str| { | |
| 472 | ui.leaf( | |
| 473 | part, | |
| 474 | Spec { | |
| 475 | size: [fill(), px(ROW)], | |
| 476 | text: Some(text), | |
| 477 | color: Some(theme.text_dim), | |
| 478 | ..Spec::default() | |
| 479 | }, | |
| 480 | ); | |
| 481 | }; | |
| 482 | caption(ui, "format", "Format"); | |
| 483 | ui.leaf( | |
| 484 | "name-label", | |
| 485 | Spec { | |
| 486 | size: [fill(), px(ROW)], | |
| 487 | text: Some("Display name:"), | |
| 488 | ..Spec::default() | |
| 489 | }, | |
| 490 | ); | |
| 491 | ui::text_field( | |
| 492 | ui, | |
| 493 | name_field(), | |
| 494 | &mut tag.label, | |
| 495 | "", | |
| 496 | Spec { | |
| 497 | size: [fill(), px(ROW + 4.0)], | |
| 498 | fill: Some(theme.base), | |
| 499 | border: Some(theme.accent), | |
| 500 | radius: 4.0, | |
| 501 | pad: [6.0, 0.0], | |
| 502 | ..Spec::default() | |
| 503 | }, | |
| 504 | ); | |
| 505 | ui.open( | |
| 506 | "pickers", | |
| 507 | Spec { | |
| 508 | size: [fill(), children()], | |
| 509 | gap: 10.0, | |
| 510 | ..Spec::default() | |
| 511 | }, | |
| 512 | ); | |
| 513 | let column = |ui: &mut Ui, part: &str, text: &str| { | |
| 514 | ui.open( | |
| 515 | part, | |
| 516 | Spec { | |
| 517 | axis: Axis::Y, | |
| 518 | size: [children(), children()], | |
| 519 | gap: 2.0, | |
| 520 | ..Spec::default() | |
| 521 | }, | |
| 522 | ); | |
| 523 | ui.leaf( | |
| 524 | "label", | |
| 525 | Spec { | |
| 526 | size: [fit(), px(ROW)], | |
| 527 | text: Some(text), | |
| 528 | ..Spec::default() | |
| 529 | }, | |
| 530 | ); | |
| 531 | }; | |
| 532 | column(ui, "symbol", "Symbol:"); | |
| 533 | let symbol = artwork(tag).unwrap_or(crate::art::FONT_COLOR); | |
| 534 | let tint = if tag.shape == 0 { theme.text } else { [1.0; 4] }; | |
| 535 | let anchor = picker(ui, "picker", Some(symbol), None, popup("symbol"), tint); | |
| 536 | if let Some(shape) = gallery(ui, anchor) { | |
| 537 | tag.shape = shape; | |
| 538 | } | |
| 539 | ui.close(); | |
| 540 | column(ui, "color", "Font Color:"); | |
| 541 | let bar = tag.color.map_or(theme.text, colorref); | |
| 542 | let anchor = picker( | |
| 543 | ui, | |
| 544 | "picker", | |
| 545 | Some(crate::art::FONT_COLOR), | |
| 546 | Some(bar), | |
| 547 | popup("color"), | |
| 548 | theme.text, | |
| 549 | ); | |
| 550 | if let Some(color) = swatch(ui, "color", anchor, "Automatic", &FONT_COLORS, 8) { | |
| 551 | tag.color = color; | |
| 552 | } | |
| 553 | ui.close(); | |
| 554 | column(ui, "highlight", "Highlight Color:"); | |
| 555 | let bar = tag.highlight.map(colorref); | |
| 556 | let anchor = picker( | |
| 557 | ui, | |
| 558 | "picker", | |
| 559 | Some(crate::art::HIGHLIGHTER), | |
| 560 | bar, | |
| 561 | popup("highlight"), | |
| 562 | theme.text, | |
| 563 | ); | |
| 564 | if let Some(color) = swatch(ui, "highlight", anchor, "None", &HIGHLIGHTS, 5) { | |
| 565 | tag.highlight = color; | |
| 566 | } | |
| 567 | ui.close(); | |
| 568 | ui.close(); | |
| 569 | caption(ui, "preview-label", "Preview"); | |
| 570 | ui.open( | |
| 571 | "preview", | |
| 572 | Spec { | |
| 573 | size: [fill(), px(ROW * 2.0)], | |
| 574 | fill: Some(theme.base), | |
| 575 | border: Some(theme.chip), | |
| 576 | radius: 4.0, | |
| 577 | pad: [8.0, ROW / 2.0], | |
| 578 | ..Spec::default() | |
| 579 | }, | |
| 580 | ); | |
| 581 | ui.leaf( | |
| 582 | "space", | |
| 583 | Spec { | |
| 584 | size: [fill(), px(1.0)], | |
| 585 | ..Spec::default() | |
| 586 | }, | |
| 587 | ); | |
| 588 | icon(ui, "icon", artwork(tag)); | |
| 589 | label(ui, "label", &tag.label, tag); | |
| 590 | ui.leaf( | |
| 591 | "end", | |
| 592 | Spec { | |
| 593 | size: [fill(), px(1.0)], | |
| 594 | ..Spec::default() | |
| 595 | }, | |
| 596 | ); | |
| 597 | ui.close(); | |
| 598 | ui.leaf( | |
| 599 | "note", | |
| 600 | Spec { | |
| 601 | size: [fill(), fit()], | |
| 602 | text: Some("Customizations do not affect notes you have already tagged."), | |
| 603 | color: Some(theme.text_dim), | |
| 604 | overflow: ui::Overflow::Wrap, | |
| 605 | ..Spec::default() | |
| 606 | }, | |
| 607 | ); | |
| 608 | buttons(ui); | |
| 609 | let named = !tag.label.trim().is_empty(); | |
| 610 | let ok = button(ui, "ok", "OK", named) || entered && named; | |
| 611 | let cancel = ui::button(ui, "cancel", "Cancel").clicked; | |
| 612 | ui.close(); | |
| 613 | ui.close(); | |
| 614 | if ok { | |
| 615 | ui.close_popup(editor_id()); | |
| 616 | return editor.take().map(|(mut tag, place)| { | |
| 617 | tag.label = tag.label.trim().to_owned(); | |
| 618 | (tag, place) | |
| 619 | }); | |
| 620 | } | |
| 621 | if cancel { | |
| 622 | ui.close_popup(editor_id()); | |
| 623 | *editor = None; | |
| 624 | } | |
| 625 | None | |
| 626 | } | |
| 627 | ||
| 628 | /// Symbol's gallery below `anchor` while it is open. Returns the symbol picked, 0 for None. | |
| 629 | fn gallery(ui: &mut Ui, anchor: Anchor) -> Option<u16> { | |
| 630 | let id = popup("symbol"); | |
| 631 | if !ui.popup_open(id) { | |
| 632 | return None; | |
| 633 | } | |
| 634 | let theme = ui.theme.clone(); | |
| 635 | ui.open_as( | |
| 636 | id, | |
| 637 | Spec { | |
| 638 | axis: Axis::Y, | |
| 639 | size: [children(), children()], | |
| 640 | fill: Some(theme.popup), | |
| 641 | border: Some(theme.chip), | |
| 642 | shadow: Some(theme.shadow), | |
| 643 | radius: 6.0, | |
| 644 | pad: [6.0, 6.0], | |
| 645 | anchor: Some(anchor), | |
| 646 | ..Spec::default() | |
| 647 | }, | |
| 648 | ); | |
| 649 | let mut picked = None; | |
| 650 | for (row, shapes) in GALLERY.iter().enumerate() { | |
| 651 | ui.open( | |
| 652 | ("row", row), | |
| 653 | Spec { | |
| 654 | size: [children(), px(CELL)], | |
| 655 | ..Spec::default() | |
| 656 | }, | |
| 657 | ); | |
| 658 | for (column, &shape) in shapes.iter().enumerate() { | |
| 659 | // A rule between the groups of three. | |
| 660 | let gap = if column % 3 == 0 && column > 0 { | |
| 661 | 8.0 | |
| 662 | } else { | |
| 663 | 0.0 | |
| 664 | }; | |
| 665 | if gap > 0.0 { | |
| 666 | ui.leaf( | |
| 667 | ("gap", column), | |
| 668 | Spec { | |
| 669 | size: [px(gap), px(CELL)], | |
| 670 | ..Spec::default() | |
| 671 | }, | |
| 672 | ); | |
| 673 | } | |
| 674 | if shape == 0 && row == 0 && column > 0 { | |
| 675 | continue; | |
| 676 | } | |
| 677 | let none = shape == 0 && row == 0; | |
| 678 | let spec = if none { | |
| 679 | Spec { | |
| 680 | flags: Flags::CLICKABLE, | |
| 681 | size: [px(CELL * 3.0), px(CELL)], | |
| 682 | text: Some("None"), | |
| 683 | hover_fill: Some(theme.hover()), | |
| 684 | radius: 3.0, | |
| 685 | center: true, | |
| 686 | ..Spec::default() | |
| 687 | } | |
| 688 | } else if shape == 0 { | |
| 689 | Spec { | |
| 690 | size: [px(CELL), px(CELL)], | |
| 691 | ..Spec::default() | |
| 692 | } | |
| 693 | } else { | |
| 694 | Spec { | |
| 695 | flags: Flags::CLICKABLE, | |
| 696 | size: [px(CELL), px(CELL)], | |
| 697 | icon: TagIcon::of(shape, false).map(tag_sources), | |
| 698 | color: Some([1.0; 4]), | |
| 699 | hover_fill: Some(theme.hover()), | |
| 700 | radius: 3.0, | |
| 701 | center: true, | |
| 702 | ..Spec::default() | |
| 703 | } | |
| 704 | }; | |
| 705 | if ui.leaf(("cell", column), spec).clicked && (none || shape != 0) { | |
| 706 | picked = Some(shape); | |
| 707 | } | |
| 708 | if let Some(name) = symbol_name(shape) { | |
| 709 | ui::popup::tooltip(ui, name, "", None); | |
| 710 | } | |
| 711 | } | |
| 712 | ui.close(); | |
| 713 | } | |
| 714 | ui.close(); | |
| 715 | if picked.is_some() { | |
| 716 | ui.close_popup(id); | |
| 717 | } | |
| 718 | picked | |
| 719 | } | |
| 720 | ||
| 721 | /// A colour popup of `swatches` below `anchor` while it is open. Returns the COLORREF | |
| 722 | /// picked, or `Some(None)` for the button labelled `none`. | |
| 723 | fn swatch( | |
| 724 | ui: &mut Ui, | |
| 725 | name: &str, | |
| 726 | anchor: Anchor, | |
| 727 | none: &str, | |
| 728 | swatches: &[u32], | |
| 729 | columns: usize, | |
| 730 | ) -> Option<Option<u32>> { | |
| 731 | let colors: Vec<_> = swatches.iter().map(|color| colorref(*color)).collect(); | |
| 732 | let chosen = ui::popup::colors(ui, popup(name), anchor, none, &colors, columns)?; | |
| 733 | Some(chosen.and_then(|chosen| { | |
| 734 | swatches | |
| 735 | .iter() | |
| 736 | .zip(&colors) | |
| 737 | .find(|(_, color)| **color == chosen) | |
| 738 | .map(|(stored, _)| *stored) | |
| 739 | })) | |
| 740 | } | |
| 741 | ||
| 742 | #[cfg(test)] | |
| 743 | mod tests { | |
| 744 | use super::*; | |
| 745 | ||
| 746 | /// The gallery offers each symbol OneNote 2010's does once: every one MS-ONE lists but | |
| 747 | /// the follow-up flags it keeps for Outlook tasks. | |
| 748 | /// OneNote 2010's gallery, cell by cell: its page "Cells" tags a paragraph with a tag | |
| 749 | /// named for each cell's row and column, its symbol picked from that cell | |
| 750 | /// (`corpus/custom-tags/native`). | |
| 751 | #[test] | |
| 752 | fn the_gallery_lays_symbols_out_as_onenotes_does() { | |
| 753 | use onestore::{ | |
| 754 | RevisionIndex, Store, | |
| 755 | document::{Document, Kind}, | |
| 756 | page::Page, | |
| 757 | }; | |
| 758 | let bytes = include_bytes!("../../../corpus/custom-tags/native/shapes.one"); | |
| 759 | let store = Store::parse(bytes).unwrap(); | |
| 760 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 761 | let document = Document::parse(&index).unwrap(); | |
| 762 | let cells = document | |
| 763 | .pages() | |
| 764 | .unwrap() | |
| 765 | .into_iter() | |
| 766 | .map(|(space, _)| Page::from_space(&document, space).unwrap()) | |
| 767 | .find(|page| page.title.starts_with("Cells")) | |
| 768 | .unwrap(); | |
| 769 | let picked: std::collections::BTreeMap<String, u16> = cells | |
| 770 | .definitions | |
| 771 | .values() | |
| 772 | .filter_map(|definition| match &definition.kind { | |
| 773 | Kind::TagDefinition { | |
| 774 | label: Some(label), | |
| 775 | shape: Some(shape), | |
| 776 | .. | |
| 777 | } => Some((label.clone(), *shape)), | |
| 778 | _ => None, | |
| 779 | }) | |
| 780 | .collect(); | |
| 781 | let mut cells = 0; | |
| 782 | for (row, shapes) in GALLERY.iter().enumerate() { | |
| 783 | for (column, shape) in shapes.iter().enumerate() { | |
| 784 | let stored = picked.get(&format!("c{row}-{column}")); | |
| 785 | assert_eq!( | |
| 786 | stored.copied().unwrap_or(0), | |
| 787 | *shape, | |
| 788 | "row {row}, column {column}" | |
| 789 | ); | |
| 790 | cells += usize::from(stored.is_some()); | |
| 791 | } | |
| 792 | } | |
| 793 | assert_eq!(cells, 138); | |
| 794 | } | |
| 795 | ||
| 796 | #[test] | |
| 797 | fn the_gallery_offers_every_symbol_once() { | |
| 798 | let mut offered: Vec<u16> = GALLERY | |
| 799 | .iter() | |
| 800 | .flatten() | |
| 801 | .copied() | |
| 802 | .filter(|shape| *shape != 0) | |
| 803 | .collect(); | |
| 804 | offered.sort_unstable(); | |
| 805 | let expected: Vec<u16> = (1..=143) | |
| 806 | .filter(|shape| !(89..=93).contains(shape)) | |
| 807 | .collect(); | |
| 808 | assert_eq!(offered, expected); | |
| 809 | } | |
| 810 | } |
crates/ui/src/lib.rs+36-5| ... | ... | @@ -606,6 +606,12 @@ enum Display { |
| 606 | 606 | /// Top and bottom. |
| 607 | 607 | colors: [[f32; 4]; 2], |
| 608 | 608 | }, |
| 609 | Shadow { | |
| 610 | rect: [f32; 4], | |
| 611 | radius: f32, | |
| 612 | blur: f32, | |
| 613 | color: [f32; 4], | |
| 614 | }, | |
| 609 | 615 | Icon { |
| 610 | 616 | sources: &'static [&'static str], |
| 611 | 617 | origin: [f32; 2], |
| ... | ... | @@ -1452,12 +1458,26 @@ impl Ui { |
| 1452 | 1458 | spread, |
| 1453 | 1459 | } in shadows |
| 1454 | 1460 | { |
| 1461 | let origin = [painted[0] - spread, painted[1] + drop - spread]; | |
| 1455 | 1462 | let grown = size.map(|side| side + 2.0 * spread); |
| 1456 | self.display.push(Display::Path { | |
| 1457 | data: outline(node.shape, grown, node.radius + spread), | |
| 1458 | origin: [painted[0] - spread, painted[1] + drop - spread], | |
| 1459 | style: PathStyle::Shadow(blur), | |
| 1460 | colors: [color; 2], | |
| 1463 | self.display.push(match node.shape { | |
| 1464 | Shape::Rounded => Display::Shadow { | |
| 1465 | rect: [ | |
| 1466 | origin[0], | |
| 1467 | origin[1], | |
| 1468 | origin[0] + grown[0], | |
| 1469 | origin[1] + grown[1], | |
| 1470 | ], | |
| 1471 | radius: node.radius + spread, | |
| 1472 | blur, | |
| 1473 | color, | |
| 1474 | }, | |
| 1475 | Shape::Tab { .. } => Display::Path { | |
| 1476 | data: outline(node.shape, grown, node.radius + spread), | |
| 1477 | origin, | |
| 1478 | style: PathStyle::Shadow(blur), | |
| 1479 | colors: [color; 2], | |
| 1480 | }, | |
| 1461 | 1481 | }); |
| 1462 | 1482 | } |
| 1463 | 1483 | } |
| ... | ... | @@ -1723,6 +1743,17 @@ impl Ui { |
| 1723 | 1743 | style: *style, |
| 1724 | 1744 | colors: *colors, |
| 1725 | 1745 | }), |
| 1746 | &Display::Shadow { | |
| 1747 | rect, | |
| 1748 | radius, | |
| 1749 | blur, | |
| 1750 | color, | |
| 1751 | } => primitives.push(Primitive::Shadow { | |
| 1752 | rect, | |
| 1753 | radius, | |
| 1754 | blur, | |
| 1755 | color, | |
| 1756 | }), | |
| 1726 | 1757 | Display::Segment { from, to, color } => primitives.push(Primitive::Segment { |
| 1727 | 1758 | from: *from, |
| 1728 | 1759 | to: *to, |
crates/ui/src/popup.rs+57-13| ... | ... | @@ -27,6 +27,8 @@ const PALETTE: f32 = 560.0; |
| 27 | 27 | const TIP_WIDTH: f32 = 280.0; |
| 28 | 28 | /// A colour grid's cell, around its swatch. |
| 29 | 29 | const CELL: f32 = 22.0; |
| 30 | /// Inset of an item's text from the highlight it previews. | |
| 31 | const SAMPLE_PAD: f32 = 2.0; | |
| 30 | 32 | /// Seconds a popup takes to ease to the height of its results. |
| 31 | 33 | const RESIZE: f32 = 0.15; |
| 32 | 34 | thread_local! { |
| ... | ... | @@ -50,6 +52,10 @@ pub struct Item<'a> { |
| 50 | 52 | pub text: &'a str, |
| 51 | 53 | /// The family the text previews, as in a font menu. |
| 52 | 54 | pub font: Option<&'a str>, |
| 55 | /// The colour and highlight the text previews, as a tag without a symbol shows in | |
| 56 | /// OneNote's Tags menu. | |
| 57 | pub ink: Option<[f32; 4]>, | |
| 58 | pub highlight: Option<[f32; 4]>, | |
| 53 | 59 | pub icon: Option<&'static [&'static str]>, |
| 54 | 60 | /// The icon has colours of its own, so the text's colour does not tint it. |
| 55 | 61 | pub colored: bool, |
| ... | ... | @@ -87,7 +93,14 @@ pub fn menu( |
| 87 | 93 | }; |
| 88 | 94 | let [text, shortcut] = items.iter().fold([0.0_f32; 2], |[text, shortcut], item| { |
| 89 | 95 | [ |
| 90 | text.max(measure(item.text)), | |
| 96 | text.max( | |
| 97 | measure(item.text) | |
| 98 | + if item.highlight.is_some() { | |
| 99 | 2.0 * SAMPLE_PAD | |
| 100 | } else { | |
| 101 | 0.0 | |
| 102 | }, | |
| 103 | ), | |
| 91 | 104 | shortcut.max(measure(item.shortcut)), |
| 92 | 105 | ] |
| 93 | 106 | }); |
| ... | ... | @@ -414,18 +427,49 @@ fn menu_row(ui: &mut Ui, style: &Menu, matches: &Matches, row: Row) { |
| 414 | 427 | }, |
| 415 | 428 | ); |
| 416 | 429 | } |
| 417 | ui.leaf( | |
| 418 | "text", | |
| 419 | Spec { | |
| 420 | size: [fill(), fill()], | |
| 421 | text: Some(item.text), | |
| 422 | font: item.font, | |
| 423 | font_size: Some(style.font_size - if item.heading { 2.0 } else { 0.0 }), | |
| 424 | bold: item.heading, | |
| 425 | color: Some(color), | |
| 426 | ..Spec::default() | |
| 427 | }, | |
| 428 | ); | |
| 430 | let text = Spec { | |
| 431 | size: [fill(), fill()], | |
| 432 | text: Some(item.text), | |
| 433 | font: item.font, | |
| 434 | font_size: Some(style.font_size - if item.heading { 2.0 } else { 0.0 }), | |
| 435 | bold: item.heading, | |
| 436 | color: Some(match (item.ink, item.highlight) { | |
| 437 | (None, None) => color, | |
| 438 | // Automatic text on a highlight is black, as on OneNote's page. | |
| 439 | (ink, highlight) => { | |
| 440 | let ink = ink.unwrap_or([0.0, 0.0, 0.0, 1.0]); | |
| 441 | if item.disabled { | |
| 442 | mix(ink, highlight.unwrap_or(style.fill), 0.5) | |
| 443 | } else { | |
| 444 | ink | |
| 445 | } | |
| 446 | } | |
| 447 | }), | |
| 448 | ..Spec::default() | |
| 449 | }; | |
| 450 | match item.highlight { | |
| 451 | Some(highlight) => { | |
| 452 | ui.leaf( | |
| 453 | "text", | |
| 454 | Spec { | |
| 455 | size: [fit(), px(style.row - 4.0)], | |
| 456 | fill: Some(highlight), | |
| 457 | pad: [SAMPLE_PAD, 0.0], | |
| 458 | ..text | |
| 459 | }, | |
| 460 | ); | |
| 461 | ui.leaf( | |
| 462 | "rest", | |
| 463 | Spec { | |
| 464 | size: [fill(), fill()], | |
| 465 | ..Spec::default() | |
| 466 | }, | |
| 467 | ); | |
| 468 | } | |
| 469 | None => { | |
| 470 | ui.leaf("text", text); | |
| 471 | } | |
| 472 | } | |
| 429 | 473 | if !item.shortcut.is_empty() { |
| 430 | 474 | ui.leaf( |
| 431 | 475 | "shortcut", |
tools/native/read.ps1+3-1| ... | ... | @@ -99,7 +99,9 @@ try { |
| 99 | 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) |
| 100 | 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() |
| 101 | 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 102 | # A paragraph's file is named by its paragraph, one on the page by itself. | |
| 103 | $owner = if ($file.ParentNode.LocalName -eq 'Page') { $file } else { $file.ParentNode } | |
| 104 | $payloads += @{ page = $id; object = $owner.GetAttribute('objectID'); | |
| 103 | 105 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); |
| 104 | 106 | sha256 = $hash; bytes = $bytes.Length } |
| 105 | 107 | } |
tools/test_attachment_floating.py created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | from pathlib import Path | |
| 2 | import hashlib | |
| 3 | import json | |
| 4 | import runpy | |
| 5 | import shutil | |
| 6 | from tempfile import TemporaryDirectory | |
| 7 | import unittest | |
| 8 | import xml.etree.ElementTree as ET | |
| 9 | ||
| 10 | from native_xml import ns | |
| 11 | ||
| 12 | ROOT = Path(__file__).resolve().parent.parent | |
| 13 | FIXTURE = ROOT / 'corpus/attachment-floating' | |
| 14 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | |
| 15 | ||
| 16 | ||
| 17 | class AttachmentFloatingTest(unittest.TestCase): | |
| 18 | def test_files_the_editor_put_on_the_page_open_in_onenote_where_they_were_placed(self): | |
| 19 | read = FIXTURE / 'cold/read' | |
| 20 | with TemporaryDirectory() as temporary: | |
| 21 | copy = Path(temporary) / 'read' | |
| 22 | shutil.copytree(read, copy) | |
| 23 | compare(FIXTURE / 'candidate', copy) | |
| 24 | page, = (ET.parse(path).getroot() for path in sorted(read.glob('page-*.xml'))) | |
| 25 | self.assertEqual(page.findall('.//one:OE/one:InsertedFile', ns), []) | |
| 26 | placed = {} | |
| 27 | for inserted in page.findall('one:InsertedFile', ns): | |
| 28 | position = inserted.find('one:Position', ns) | |
| 29 | placed[inserted.get('preferredName')] = ( | |
| 30 | round(float(position.get('x')), 2), round(float(position.get('y')), 2)) | |
| 31 | # Attached at the caret a click on blank page left, the second then dragged. | |
| 32 | self.assertEqual(placed, {'float 🦀.txt': (342.0, 284.4), 'large.bin': (288.0, 140.4)}) | |
| 33 | # The bytes OneNote hands an application when the file is opened. | |
| 34 | payloads = json.loads((read / 'payloads.json').read_text(encoding='utf-8-sig')) | |
| 35 | self.assertEqual(sorted((p['name'], p['bytes']) for p in payloads), | |
| 36 | [('float 🦀.txt', 37), ('large.bin', 2 << 20)]) | |
| 37 | for payload in payloads: | |
| 38 | data = (read / (payload['sha256'] + '.attachment')).read_bytes() | |
| 39 | self.assertEqual(hashlib.sha256(data).hexdigest(), payload['sha256']) | |
| 40 | ||
| 41 | ||
| 42 | if __name__ == '__main__': | |
| 43 | unittest.main() |
tools/test_attachment_insert.py created+42| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | from pathlib import Path | |
| 2 | import hashlib | |
| 3 | import json | |
| 4 | import re | |
| 5 | import runpy | |
| 6 | import shutil | |
| 7 | from tempfile import TemporaryDirectory | |
| 8 | import unittest | |
| 9 | import xml.etree.ElementTree as ET | |
| 10 | ||
| 11 | from native_xml import ns | |
| 12 | ||
| 13 | ROOT = Path(__file__).resolve().parent.parent | |
| 14 | FIXTURE = ROOT / 'corpus/attachment-insert' | |
| 15 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | |
| 16 | ||
| 17 | ||
| 18 | class AttachmentInsertTest(unittest.TestCase): | |
| 19 | def test_files_attached_through_the_editor_open_in_onenote_with_their_bytes(self): | |
| 20 | read = FIXTURE / 'cold/read' | |
| 21 | with TemporaryDirectory() as temporary: | |
| 22 | copy = Path(temporary) / 'read' | |
| 23 | shutil.copytree(read, copy) | |
| 24 | compare(FIXTURE / 'candidate', copy) | |
| 25 | page, = (ET.parse(path).getroot() for path in sorted(read.glob('page-*.xml'))) | |
| 26 | paragraphs = [] | |
| 27 | for element in page.findall('.//one:OE', ns): | |
| 28 | inserted = element.find('one:InsertedFile', ns) | |
| 29 | text = element.find('one:T', ns) | |
| 30 | paragraphs.append('[%s]' % inserted.get('preferredName') if inserted is not None | |
| 31 | else re.sub(r'<[^>]*>', '', text.text or '')) | |
| 32 | self.assertEqual(paragraphs, ['Before the file', '[notes 🦀.txt]', 'After the file', '[large.bin]', '']) | |
| 33 | # The bytes OneNote hands an application when the file is opened. | |
| 34 | payloads = json.loads((read / 'payloads.json').read_text(encoding='utf-8-sig')) | |
| 35 | self.assertEqual(sorted(p['bytes'] for p in payloads), [30, 2 << 20]) | |
| 36 | for payload in payloads: | |
| 37 | data = (read / (payload['sha256'] + '.attachment')).read_bytes() | |
| 38 | self.assertEqual(hashlib.sha256(data).hexdigest(), payload['sha256']) | |
| 39 | ||
| 40 | ||
| 41 | if __name__ == '__main__': | |
| 42 | unittest.main() |
tools/test_custom_tags.py created+45| ... | ... | @@ -0,0 +1,45 @@ |
| 1 | from pathlib import Path | |
| 2 | import re | |
| 3 | import runpy | |
| 4 | import shutil | |
| 5 | from tempfile import TemporaryDirectory | |
| 6 | import unittest | |
| 7 | import xml.etree.ElementTree as ET | |
| 8 | ||
| 9 | from native_xml import ns | |
| 10 | ||
| 11 | ROOT = Path(__file__).resolve().parent.parent | |
| 12 | FIXTURE = ROOT / 'corpus/custom-tags' | |
| 13 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | |
| 14 | ||
| 15 | ||
| 16 | class CustomTagsTest(unittest.TestCase): | |
| 17 | def test_customized_tags_render_natively_with_their_names_symbols_and_colours(self): | |
| 18 | with TemporaryDirectory() as temporary: | |
| 19 | read = Path(temporary) / 'read' | |
| 20 | shutil.copytree(FIXTURE / 'cold/read', read) | |
| 21 | compare(FIXTURE / 'candidate', read) | |
| 22 | page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))) | |
| 23 | definitions = [(d.get('type'), d.get('symbol'), d.get('name'), d.get('fontColor'), d.get('highlightColor')) | |
| 24 | for d in page.findall('one:TagDef', ns)] | |
| 25 | self.assertEqual(definitions, [ | |
| 26 | ('0', '61', 'Snow check', '#800000', '#00CCFF'), | |
| 27 | ('1', '48', 'Snow task', 'automatic', 'none'), | |
| 28 | ('2', '31', 'Snow circle', 'automatic', 'none'), | |
| 29 | ('3', '0', 'Snow ink', '#0000FF', 'none'), | |
| 30 | ]) | |
| 31 | types = {d.get('index'): d.get('type') for d in page.findall('one:TagDef', ns)} | |
| 32 | tagged = [(sorted((types[t.get('index')], t.get('completed')) for t in oe.findall('one:Tag', ns)), | |
| 33 | re.sub(r'<[^>]*>', '', oe.find('one:T', ns).text or '')) | |
| 34 | for oe in page.iter('{%s}OE' % ns['one']) if oe.find('one:T', ns) is not None] | |
| 35 | self.assertEqual(tagged, [ | |
| 36 | ([('0', 'true')], 'Custom symbol and colours'), | |
| 37 | ([('1', 'false')], 'Custom check box'), | |
| 38 | ([('1', 'true')], 'Custom check box, checked'), | |
| 39 | ([('2', 'true'), ('3', 'true')], 'Two custom tags'), | |
| 40 | ([], 'Untagged'), | |
| 41 | ]) | |
| 42 | ||
| 43 | ||
| 44 | if __name__ == '__main__': | |
| 45 | unittest.main() |
tools/test_page_background.py+18-8| ... | ... | @@ -24,19 +24,29 @@ class PageBackgroundTest(unittest.TestCase): |
| 24 | 24 | pages[page.get('name')] = page |
| 25 | 25 | |
| 26 | 26 | def art(title): |
| 27 | return [image.get('backgroundImage') for image in pages[title].findall('one:Image', ns)] | |
| 27 | return [(image.get('backgroundImage'), image.find('one:Size', ns).get('width')) | |
| 28 | for image in pages[title].findall('one:Image', ns)] | |
| 28 | 29 | |
| 29 | 30 | def rules(title): |
| 30 | 31 | return pages[title].find('one:PageSettings/one:RuleLines', ns).get('visible') |
| 31 | 32 | |
| 32 | # Art joins a ruled page, which keeps its lines; art taken away leaves none. | |
| 33 | self.assertEqual(art('Standard'), ['true']) | |
| 34 | self.assertEqual(rules('Standard'), 'true') | |
| 35 | self.assertEqual(art('Wide'), []) | |
| 36 | self.assertEqual(rules('Wide'), 'true') | |
| 37 | # A colour no menu offers reads back as written. | |
| 38 | self.assertEqual(pages['SmallGrid'].find('one:PageSettings', ns).get('color'), '#E8D8C8') | |
| 33 | def color(title): | |
| 34 | return pages[title].find('one:PageSettings', ns).get('color') | |
| 39 | 35 | |
| 36 | ivy, sparks = [('true', '174.5')], [('true', '611.4000244140625')] | |
| 37 | # Art joins a ruled page, which keeps its lines; art taken away or undone leaves none; | |
| 38 | # redone, or undone after other art replaced it, the art is back. | |
| 39 | self.assertEqual(art('Standard'), ivy) | |
| 40 | self.assertEqual(art('Wide'), []) | |
| 41 | self.assertEqual(art('College'), []) | |
| 42 | self.assertEqual(art('MediumGrid'), ivy) | |
| 43 | self.assertEqual(art('LargeGrid'), ivy) | |
| 44 | for title in ('Standard', 'Wide', 'College', 'MediumGrid', 'LargeGrid', 'VeryLargeGrid'): | |
| 45 | self.assertEqual(rules(title), 'true', title) | |
| 46 | # A colour no menu offers reads back as written; art over a colour keeps it. | |
| 47 | self.assertEqual(color('SmallGrid'), '#E8D8C8') | |
| 48 | self.assertEqual(art('VeryLargeGrid'), sparks) | |
| 49 | self.assertEqual(color('VeryLargeGrid'), '#FDFDDD') | |
| 40 | 50 | |
| 41 | 51 | if __name__ == '__main__': |
| 42 | 52 | unittest.main() |
tools/verify-document.py+2-1| ... | ... | @@ -178,7 +178,8 @@ def compare_objects(space, roots, page, native_roots, assets, native_payloads, a |
| 178 | 178 | frame_size = stored_size + (1.44 if kind['printout'] else 0) |
| 179 | 179 | assert abs(frame_size - native_size) < .002, ('Image display dimension differs', axis, frame_size, native_size) |
| 180 | 180 | else: |
| 181 | expected = [p for p in native_payloads if p['page'] == page.get('ID') and p['object'] == parents[native].get('objectID')] | |
| 181 | owner = native if parents[native] is page else parents[native] | |
| 182 | expected = [p for p in native_payloads if p['page'] == page.get('ID') and p['object'] == owner.get('objectID')] | |
| 182 | 183 | assert len(expected) == 1, 'Native attachment association is ambiguous' |
| 183 | 184 | assert hashlib.sha256(data).hexdigest() == expected[0]['sha256'], 'Associated attachment bytes differ' |
| 184 | 185 | assert kind['filename'] == expected[0]['name'], 'Attachment filename differs' |