| 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,6 +1888,7 @@ dependencies = [ |
| 1888 | "onestore", | 1888 | "onestore", |
| 1889 | "parley", | 1889 | "parley", |
| 1890 | "pollster", | 1890 | "pollster", |
| 1891 | "resvg", | ||
| 1891 | "serde", | 1892 | "serde", |
| 1892 | "serde_json", | 1893 | "serde_json", |
| 1893 | "tempfile", | 1894 | "tempfile", |
apps/ios/Snowbound/App.swift+33-11| ... | @@ -19,6 +19,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { | ... | @@ -19,6 +19,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { |
| 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? | 19 | _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? |
| 20 | ) -> Bool { | 20 | ) -> Bool { |
| 21 | sb_set_coordinator(coordinate) | 21 | sb_set_coordinator(coordinate) |
| 22 | sb_set_sync_wake { DispatchQueue.main.async { NotificationCenter.default.post(name: Sync.changed, object: nil) } } | ||
| 22 | #if DEBUG | 23 | #if DEBUG |
| 23 | Presenter.watch() | 24 | Presenter.watch() |
| 24 | #endif | 25 | #endif |
| ... | @@ -77,11 +78,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro | ... | @@ -77,11 +78,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 77 | split.setViewController(pages, for: .supplementary) | 78 | split.setViewController(pages, for: .supplementary) |
| 78 | split.setViewController(UIViewController(), for: .secondary) | 79 | split.setViewController(UIViewController(), for: .secondary) |
| 79 | notebooks.onOpen = { [weak self] tab, notebook in self?.open(tab, of: notebook) } | 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.onOpen = { [weak self] section, id in self?.show(section: section, page: id) } |
| 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 | } | ||
| 85 | window.rootViewController = split | 82 | window.rootViewController = split |
| 86 | window.makeKeyAndVisible() | 83 | window.makeKeyAndVisible() |
| 87 | self.window = window | 84 | self.window = window |
| ... | @@ -116,11 +113,13 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro | ... | @@ -116,11 +113,13 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 116 | } | 113 | } |
| 117 | 114 | ||
| 118 | func open( | 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 | then: ((Section) -> Void)? = nil | 117 | then: ((Section) -> Void)? = nil |
| 121 | ) { | 118 | ) { |
| 122 | guard Author.name != nil else { | 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 | pages.loading(tab) | 124 | pages.loading(tab) |
| 126 | split.show(.supplementary) | 125 | split.show(.supplementary) |
| ... | @@ -134,15 +133,37 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro | ... | @@ -134,15 +133,37 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro |
| 134 | notebooks.select(tab, of: notebook) | 133 | notebooks.select(tab, of: notebook) |
| 135 | Place.saved = Place(notebook: notebook.id, section: tab.path, page: page) | 134 | Place.saved = Place(notebook: notebook.id, section: tab.path, page: page) |
| 136 | if let page, section.row(of: page) != nil { | 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 | then?(section) | 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 | showingPage = true | 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 | controller.onOpen = { [weak self] id in self?.show(section: section, page: id) } | 167 | controller.onOpen = { [weak self] id in self?.show(section: section, page: id) } |
| 147 | split.setViewController(UINavigationController(rootViewController: controller), for: .secondary) | 168 | split.setViewController(UINavigationController(rootViewController: controller), for: .secondary) |
| 148 | split.show(.secondary) | 169 | split.show(.secondary) |
| ... | @@ -204,7 +225,8 @@ final class RootViewController: UISplitViewController { | ... | @@ -204,7 +225,8 @@ final class RootViewController: UISplitViewController { |
| 204 | [ | 225 | [ |
| 205 | UIKeyCommand(title: "New Page", action: #selector(newPage), input: "n", modifierFlags: .command), | 226 | UIKeyCommand(title: "New Page", action: #selector(newPage), input: "n", modifierFlags: .command), |
| 206 | UIKeyCommand( | 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 | UIKeyCommand(title: "Search", action: #selector(search), input: "f", modifierFlags: [.command, .alternate]), | 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,6 +106,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 106 | /// gestures move; the canvas paints neither. | 106 | /// gestures move; the canvas paints neither. |
| 107 | private lazy var display = UITextSelectionDisplayInteraction(textInput: self, delegate: self) | 107 | private lazy var display = UITextSelectionDisplayInteraction(textInput: self, delegate: self) |
| 108 | private let history = CanvasUndoManager() | 108 | private let history = CanvasUndoManager() |
| 109 | private let spaceHint = Hint() | ||
| 109 | 110 | ||
| 110 | weak var inputDelegate: UITextInputDelegate? | 111 | weak var inputDelegate: UITextInputDelegate? |
| 111 | lazy var tokenizer: UITextInputTokenizer = LineTokenizer(canvas: self) | 112 | lazy var tokenizer: UITextInputTokenizer = LineTokenizer(canvas: self) |
| ... | @@ -134,6 +135,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -134,6 +135,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 134 | minimumZoomScale = 0.25 | 135 | minimumZoomScale = 0.25 |
| 135 | maximumZoomScale = 4 | 136 | maximumZoomScale = 4 |
| 136 | keyboardDismissMode = .interactive | 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 | addSubview(content) | 140 | addSubview(content) |
| 138 | insertSubview(metal, at: 0) | 141 | insertSubview(metal, at: 0) |
| 139 | // Touches land on the scroll view itself, where the text interaction looks for them. | 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,6 +490,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 487 | default: | 490 | default: |
| 488 | display.isActivated = isFirstResponder | 491 | display.isActivated = isFirstResponder |
| 489 | edit(external: true) { sb_view_release(handle) } | 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,7 +516,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 512 | 516 | ||
| 513 | /// Tells the bar buttons and the format bar what the selection now has. | 517 | /// Tells the bar buttons and the format bar what the selection now has. |
| 514 | private func changedSelection() { | 518 | private func changedSelection() { |
| 515 | if let handle { formatBar.show(sb_view_format(handle)) } | 519 | formatBar.show(format) |
| 516 | onChange?() | 520 | onChange?() |
| 517 | } | 521 | } |
| 518 | 522 | ||
| ... | @@ -576,14 +580,20 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -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 | override func copy(_ sender: Any?) { | 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 | UIPasteboard.general.string = text | 586 | UIPasteboard.general.string = text |
| 582 | } | 587 | } |
| 583 | 588 | ||
| 584 | override func cut(_ sender: Any?) { | 589 | override func cut(_ sender: Any?) { |
| 585 | copy(sender) | 590 | guard let handle else { return } |
| 586 | delete(sender) | 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 | override func delete(_ sender: Any?) { | 599 | override func delete(_ sender: Any?) { |
| ... | @@ -614,6 +624,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -614,6 +624,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 614 | override func toggleItalics(_ sender: Any?) { apply(1) } | 624 | override func toggleItalics(_ sender: Any?) { apply(1) } |
| 615 | override func toggleUnderline(_ sender: Any?) { apply(2) } | 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 | // MARK: Page | 644 | // MARK: Page |
| 618 | 645 | ||
| 619 | /// A conflict page, which shows what is stored and takes no edits. | 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,6 +651,55 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 624 | 651 | ||
| 625 | var pageText: String { handle.flatMap { take(sb_view_page_text($0)) } ?? "" } | 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 | /// Applies `sb_view_apply` formatting to the selection. | 703 | /// Applies `sb_view_apply` formatting to the selection. |
| 628 | func apply(_ command: UInt8) { | 704 | func apply(_ command: UInt8) { |
| 629 | guard let handle else { return } | 705 | guard let handle else { return } |
| ... | @@ -682,26 +758,38 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -682,26 +758,38 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 682 | override var keyCommands: [UIKeyCommand]? { | 758 | override var keyCommands: [UIKeyCommand]? { |
| 683 | guard isFirstResponder else { return nil } | 759 | guard isFirstResponder else { return nil } |
| 684 | // Ctrl+1 to Ctrl+9 in OneNote, where the Mac's Command stands for Control. | 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 | 761 | let tags = Tags.all.prefix(Tags.keyed).enumerated().map { index, tag in |
| 686 | let command = UIKeyCommand( | 762 | UIKeyCommand( |
| 687 | title: name, action: #selector(tagged), input: "\(index + 1)", modifierFlags: .command, | 763 | title: tag.name, action: #selector(formatted), input: "\(index + 1)", modifierFlags: .command, |
| 688 | propertyList: index) | 764 | propertyList: 16 + index) |
| 689 | command.wantsPriorityOverSystemBehavior = true | 765 | } |
| 690 | return command | 766 | // The desktop's chords, as OneNote for Mac's. |
| 691 | } | 767 | let formats: [(String, String, UIKeyModifierFlags, Int)] = [ |
| 692 | let outdent = UIKeyCommand( | 768 | ("Strikethrough", "x", [.command, .shift], 3), |
| 693 | title: "Decrease Indent", action: #selector(outdented), input: "\t", modifierFlags: .shift) | 769 | ("Bullets", ".", .command, 4), |
| 694 | outdent.wantsPriorityOverSystemBehavior = true | 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 | let done = UIKeyCommand(title: "Done", action: #selector(finished), input: UIKeyCommand.inputEscape) | 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) { | 788 | @objc private func formatted(_ command: UIKeyCommand) { |
| 700 | guard let index = command.propertyList as? Int else { return } | 789 | guard let command = command.propertyList as? Int else { return } |
| 701 | apply(16 + UInt8(index)) | 790 | apply(UInt8(command)) |
| 702 | } | 791 | } |
| 703 | 792 | ||
| 704 | @objc private func outdented() { apply(7) } | ||
| 705 | @objc private func finished() { _ = resignFirstResponder() } | 793 | @objc private func finished() { _ = resignFirstResponder() } |
| 706 | 794 | ||
| 707 | // MARK: Coordinates | 795 | // MARK: Coordinates |
| ... | @@ -948,3 +1036,47 @@ private final class LineTokenizer: UITextInputStringTokenizer { | ... | @@ -948,3 +1036,47 @@ private final class LineTokenizer: UITextInputStringTokenizer { |
| 948 | granularity == .line || super.isPosition(position, withinTextUnit: granularity, inDirection: direction) | 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,7 +1,7 @@ |
| 1 | import UIKit | 1 | import UIKit |
| 2 | 2 | ||
| 3 | /// Formatting above the keyboard, as Notes offers it: tags and lists first, then text | 3 | /// Formatting above the keyboard, as Notes offers it: To Do first, then text styles, lists, |
| 4 | /// styles, indentation, tags and pictures, the keyboard's dismissal at the end. | 4 | /// indentation, tags and pictures, the keyboard's dismissal at the end. |
| 5 | final class FormatBar: UIInputView { | 5 | final class FormatBar: UIInputView { |
| 6 | /// `sb_view_apply` commands and `sb_view_format` bits. | 6 | /// `sb_view_apply` commands and `sb_view_format` bits. |
| 7 | private static let buttons: [(symbol: String, label: String, command: UInt8)] = [ | 7 | private static let buttons: [(symbol: String, label: String, command: UInt8)] = [ |
| ... | @@ -10,18 +10,14 @@ final class FormatBar: UIInputView { | ... | @@ -10,18 +10,14 @@ final class FormatBar: UIInputView { |
| 10 | ("italic", "Italic", 1), | 10 | ("italic", "Italic", 1), |
| 11 | ("underline", "Underline", 2), | 11 | ("underline", "Underline", 2), |
| 12 | ("strikethrough", "Strikethrough", 3), | 12 | ("strikethrough", "Strikethrough", 3), |
| 13 | ("highlighter", "Highlight", 10), | ||
| 13 | ("list.bullet", "Bullets", 4), | 14 | ("list.bullet", "Bullets", 4), |
| 14 | ("list.number", "Numbering", 5), | 15 | ("list.number", "Numbering", 5), |
| 15 | ("decrease.indent", "Decrease Indent", 7), | 16 | ("decrease.indent", "Decrease Indent", 7), |
| 16 | ("increase.indent", "Increase Indent", 6), | 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 | private var toggles: [(UIButton, UInt8)] = [] | 19 | private var toggles: [(UIButton, UInt8)] = [] |
| 20 | private var bits: UInt64 = 0 | ||
| 25 | var onApply: ((UInt8) -> Void)? | 21 | var onApply: ((UInt8) -> Void)? |
| 26 | /// Asks for a picture from the camera (true) or the photo library. | 22 | /// Asks for a picture from the camera (true) or the photo library. |
| 27 | var onPicture: ((Bool) -> Void)? | 23 | var onPicture: ((Bool) -> Void)? |
| ... | @@ -44,9 +40,11 @@ final class FormatBar: UIInputView { | ... | @@ -44,9 +40,11 @@ final class FormatBar: UIInputView { |
| 44 | row.addArrangedSubview(button) | 40 | row.addArrangedSubview(button) |
| 45 | } | 41 | } |
| 46 | let tag = Self.button("tag", "Tags") | 42 | let tag = Self.button("tag", "Tags") |
| 47 | tag.menu = UIMenu(children: Self.tags.enumerated().map { index, name in | 43 | tag.menu = UIMenu(children: [ |
| 48 | UIAction(title: name) { [weak self] _ in self?.onApply?(16 + UInt8(index)) } | 44 | UIDeferredMenuElement.uncached { [weak self] done in |
| 49 | }) | 45 | done(Tags.menu(bits: self?.bits ?? 0) { self?.onApply?($0) }) |
| 46 | } | ||
| 47 | ]) | ||
| 50 | tag.showsMenuAsPrimaryAction = true | 48 | tag.showsMenuAsPrimaryAction = true |
| 51 | row.addArrangedSubview(tag) | 49 | row.addArrangedSubview(tag) |
| 52 | let picture = Self.button("photo", "Insert Picture") | 50 | let picture = Self.button("photo", "Insert Picture") |
| ... | @@ -105,9 +103,10 @@ final class FormatBar: UIInputView { | ... | @@ -105,9 +103,10 @@ final class FormatBar: UIInputView { |
| 105 | } | 103 | } |
| 106 | 104 | ||
| 107 | /// Marks what the selection has, from `sb_view_format`. | 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 | for (button, command) in toggles { | 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,11 +39,14 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele |
| 39 | ]) | 39 | ]) |
| 40 | navigationItem.rightBarButtonItem = UIBarButtonItem( | 40 | navigationItem.rightBarButtonItem = UIBarButtonItem( |
| 41 | title: "Add Notebook", image: UIImage(systemName: "plus"), menu: add) | 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 | override func viewWillAppear(_ animated: Bool) { | 46 | override func viewWillAppear(_ animated: Bool) { |
| 45 | super.viewWillAppear(animated) | 47 | super.viewWillAppear(animated) |
| 46 | navigationController?.navigationBar.prefersLargeTitles = true | 48 | navigationController?.navigationBar.prefersLargeTitles = true |
| 49 | navigationController?.setToolbarHidden(false, animated: false) | ||
| 47 | } | 50 | } |
| 48 | 51 | ||
| 49 | func reload() { | 52 | func reload() { |
| ... | @@ -170,6 +173,10 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele | ... | @@ -170,6 +173,10 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele |
| 170 | UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) { | 173 | UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) { |
| 171 | [weak self] _ in self?.newSection(in: notebook) | 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 | UIAction(title: "Close Notebook", image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { | 180 | UIAction(title: "Close Notebook", image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { |
| 174 | [weak self] _ in | 181 | [weak self] _ in |
| 175 | Notebooks.remove(notebook) | 182 | Notebooks.remove(notebook) |
| ... | @@ -250,7 +257,7 @@ extension UIFont { | ... | @@ -250,7 +257,7 @@ extension UIFont { |
| 250 | } | 257 | } |
| 251 | 258 | ||
| 252 | /// A section's pages, subpages indented, each page's conflict pages beneath it. | 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 | private struct Item { | 261 | private struct Item { |
| 255 | let row: Row | 262 | let row: Row |
| 256 | let version: Row.Version? | 263 | let version: Row.Version? |
| ... | @@ -260,13 +267,9 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating | ... | @@ -260,13 +267,9 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 260 | private(set) var section: Section? | 267 | private(set) var section: Section? |
| 261 | private var items: [Item] = [] | 268 | private var items: [Item] = [] |
| 262 | private var selected: String? | 269 | private var selected: String? |
| 263 | private let results = SearchViewController() | 270 | private let search = SearchViewController.controller() |
| 264 | private lazy var search = UISearchController(searchResultsController: results) | ||
| 265 | private var searching: DispatchWorkItem? | ||
| 266 | /// Opens a page of the section. | 271 | /// Opens a page of the section. |
| 267 | var onOpen: ((Section, String, String?) -> Void)? | 272 | var onOpen: ((Section, String) -> Void)? |
| 268 | /// Opens a page of another section of the notebook, found by search. | ||
| 269 | var onOpenSection: ((Notebook, String, String, String?) -> Void)? | ||
| 270 | 273 | ||
| 271 | init() { | 274 | init() { |
| 272 | super.init(style: .plain) | 275 | super.init(style: .plain) |
| ... | @@ -277,22 +280,14 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating | ... | @@ -277,22 +280,14 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 277 | override func viewDidLoad() { | 280 | override func viewDidLoad() { |
| 278 | super.viewDidLoad() | 281 | super.viewDidLoad() |
| 279 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") | 282 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page") |
| 280 | search.searchResultsUpdater = self | ||
| 281 | search.searchBar.placeholder = "Search Notebook" | ||
| 282 | navigationItem.searchController = search | 283 | navigationItem.searchController = search |
| 283 | navigationItem.hidesSearchBarWhenScrolling = false | 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 | let compose = UIBarButtonItem( | 285 | let compose = UIBarButtonItem( |
| 293 | title: "New Page", image: UIImage(systemName: "square.and.pencil"), | 286 | title: "New Page", image: UIImage(systemName: "square.and.pencil"), |
| 294 | primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) }) | 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 | NotificationCenter.default.addObserver( | 291 | NotificationCenter.default.addObserver( |
| 297 | self, selector: #selector(changed), name: Section.changed, object: nil) | 292 | self, selector: #selector(changed), name: Section.changed, object: nil) |
| 298 | } | 293 | } |
| ... | @@ -398,7 +393,7 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating | ... | @@ -398,7 +393,7 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 398 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | 393 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
| 399 | guard let section else { return } | 394 | guard let section else { return } |
| 400 | selected = items[indexPath.row].id | 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 | override func tableView( | 399 | override func tableView( |
| ... | @@ -432,34 +427,6 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating | ... | @@ -432,34 +427,6 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating |
| 432 | return UIMenu(children: actions) | 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 | /// A page `sb_library_search` found. | 432 | /// A page `sb_library_search` found. |
| ... | @@ -470,42 +437,90 @@ struct Found: Decodable { | ... | @@ -470,42 +437,90 @@ struct Found: Decodable { |
| 470 | let snippet: String | 437 | let snippet: String |
| 471 | } | 438 | } |
| 472 | 439 | ||
| 473 | final class SearchViewController: UITableViewController { | 440 | /// Every open notebook's pages holding a query, grouped by notebook, as Notes searches |
| 474 | private var found: [Found] = [] | 441 | /// every folder. |
| 475 | private var sections: [String: String] = [:] | 442 | final class SearchViewController: UITableViewController, UISearchResultsUpdating { |
| 443 | private var found: [(notebook: Notebook, pages: [Found])] = [] | ||
| 476 | private var query = "" | 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 | override func viewDidLoad() { | 456 | override func viewDidLoad() { |
| 480 | super.viewDidLoad() | 457 | super.viewDidLoad() |
| 481 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "found") | 458 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "found") |
| 482 | } | 459 | } |
| 483 | 460 | ||
| 484 | func searching(_ query: String) { | 461 | func updateSearchResults(for controller: UISearchController) { |
| 485 | guard found.isEmpty else { return } | 462 | searching?.cancel() |
| 486 | var loading = UIContentUnavailableConfiguration.loading() | 463 | let query = controller.searchBar.text?.trimmingCharacters(in: .whitespaces) ?? "" |
| 487 | loading.text = "Searching…" | 464 | guard !query.isEmpty else { return show([], query: query) } |
| 488 | contentUnavailableConfiguration = loading | 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 | self.found = found | 496 | self.found = found |
| 493 | self.query = query | 497 | self.query = query |
| 494 | self.sections = sections | ||
| 495 | tableView.reloadData() | 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 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | 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 | let cell = tableView.dequeueReusableCell(withIdentifier: "found", for: indexPath) | 516 | let cell = tableView.dequeueReusableCell(withIdentifier: "found", for: indexPath) |
| 504 | var content = UIListContentConfiguration.subtitleCell() | 517 | var content = UIListContentConfiguration.subtitleCell() |
| 505 | content.text = found.title.isEmpty ? "Untitled Page" : found.title | 518 | content.text = page.title.isEmpty ? "Untitled Page" : page.title |
| 506 | let section = sections[found.section] ?? "" | 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 | content.secondaryAttributedText = highlighted( | 522 | content.secondaryAttributedText = highlighted( |
| 508 | [section, found.snippet].filter { !$0.isEmpty }.joined(separator: " · ")) | 523 | [tab?.name ?? "", page.snippet].filter { !$0.isEmpty }.joined(separator: " · ")) |
| 509 | content.secondaryTextProperties.numberOfLines = 2 | 524 | content.secondaryTextProperties.numberOfLines = 2 |
| 510 | cell.contentConfiguration = content | 525 | cell.contentConfiguration = content |
| 511 | return cell | 526 | return cell |
| ... | @@ -526,6 +541,9 @@ final class SearchViewController: UITableViewController { | ... | @@ -526,6 +541,9 @@ final class SearchViewController: UITableViewController { |
| 526 | } | 541 | } |
| 527 | 542 | ||
| 528 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | 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,10 +209,27 @@ final class Notebook { |
| 209 | if let handle { sb_library_free(handle) } | 209 | if let handle { sb_library_free(handle) } |
| 210 | handle = library | 210 | handle = library |
| 211 | self.problem = library == nil ? (problem ?? "The notebook can’t be read.") : nil | 211 | self.problem = library == nil ? (problem ?? "The notebook can’t be read.") : nil |
| 212 | follow() | ||
| 212 | reload(done: done) | 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 | /// Lists the sections again, off the main thread. | 233 | /// Lists the sections again, off the main thread. |
| 217 | func reload(done: @escaping () -> Void) { | 234 | func reload(done: @escaping () -> Void) { |
| 218 | guard let handle else { | 235 | guard let handle else { |
apps/ios/Snowbound/PageViewController.swift+98-9| ... | @@ -1,6 +1,14 @@ | ... | @@ -1,6 +1,14 @@ |
| 1 | import PhotosUI | 1 | import PhotosUI |
| 2 | import UIKit | 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 | final class PageViewController: UIViewController, PHPickerViewControllerDelegate, | 12 | final class PageViewController: UIViewController, PHPickerViewControllerDelegate, |
| 5 | UIImagePickerControllerDelegate, UINavigationControllerDelegate | 13 | UIImagePickerControllerDelegate, UINavigationControllerDelegate |
| 6 | { | 14 | { |
| ... | @@ -9,7 +17,7 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate | ... | @@ -9,7 +17,7 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 9 | 17 | ||
| 10 | private let section: Section | 18 | private let section: Section |
| 11 | private let page: String | 19 | private let page: String |
| 12 | private let find: String? | 20 | private let reveal: Reveal? |
| 13 | private let titleFocus: Bool | 21 | private let titleFocus: Bool |
| 14 | private lazy var canvas = CanvasView(section: section, page: page) | 22 | private lazy var canvas = CanvasView(section: section, page: page) |
| 15 | private let bar = ConflictBar() | 23 | private let bar = ConflictBar() |
| ... | @@ -21,14 +29,18 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate | ... | @@ -21,14 +29,18 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 21 | systemItem: .undo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: false) }) | 29 | systemItem: .undo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: false) }) |
| 22 | private lazy var redo = UIBarButtonItem( | 30 | private lazy var redo = UIBarButtonItem( |
| 23 | systemItem: .redo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: true) }) | 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 | self.section = section | 37 | self.section = section |
| 27 | self.page = page | 38 | self.page = page |
| 28 | self.find = find | 39 | self.reveal = reveal |
| 29 | self.titleFocus = titleFocus | 40 | self.titleFocus = titleFocus |
| 30 | super.init(nibName: nil, bundle: nil) | 41 | super.init(nibName: nil, bundle: nil) |
| 31 | navigationItem.largeTitleDisplayMode = .never | 42 | navigationItem.largeTitleDisplayMode = .never |
| 43 | navigationItem.rightBarButtonItems = [more] | ||
| 32 | showTitle(section.row(of: page)?.row.title ?? "") | 44 | showTitle(section.row(of: page)?.row.title ?? "") |
| 33 | } | 45 | } |
| 34 | 46 | ||
| ... | @@ -60,13 +72,21 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate | ... | @@ -60,13 +72,21 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 60 | self, selector: #selector(sectionChanged), name: Section.changed, object: section) | 72 | self, selector: #selector(sectionChanged), name: Section.changed, object: section) |
| 61 | NotificationCenter.default.addObserver(self, selector: #selector(leaving), name: Self.leaving, object: nil) | 73 | NotificationCenter.default.addObserver(self, selector: #selector(leaving), name: Self.leaving, object: nil) |
| 62 | conflictChanged() | 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 | private func opened() { | 82 | private func opened() { |
| 67 | if let find, canvas.find(find) { | 83 | let revealed = |
| 68 | _ = canvas.becomeFirstResponder() | 84 | switch reveal { |
| 69 | } else if titleFocus, canvas.focusTitle() { | 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 | _ = canvas.becomeFirstResponder() | 90 | _ = canvas.becomeFirstResponder() |
| 71 | } | 91 | } |
| 72 | } | 92 | } |
| ... | @@ -75,23 +95,92 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate | ... | @@ -75,23 +95,92 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate |
| 75 | self.title = title.isEmpty ? "Untitled Page" : title | 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 | /// Notes' checkmark ends editing and puts the keyboard away; undo and redo sit beside it. | 105 | /// Notes' checkmark ends editing and puts the keyboard away; undo and redo sit beside it. |
| 79 | private func editingChanged() { | 106 | private func editingChanged() { |
| 80 | if !canvas.readOnly, let title = canvas.pageTitle { | 107 | if !canvas.readOnly, let title = canvas.pageTitle { |
| 81 | showTitle(title) | 108 | showTitle(title) |
| 82 | section.retitle(page, title) | 109 | section.retitle(page, title) |
| 83 | } | 110 | } |
| 111 | fitTitle() | ||
| 84 | guard canvas.isFirstResponder else { | 112 | guard canvas.isFirstResponder else { |
| 85 | navigationItem.rightBarButtonItems = nil | 113 | if navigationItem.rightBarButtonItems?.first !== more { navigationItem.rightBarButtonItems = [more] } |
| 86 | return | 114 | return |
| 87 | } | 115 | } |
| 88 | undo.isEnabled = canvas.canUndo(redo: false) | 116 | undo.isEnabled = canvas.canUndo(redo: false) |
| 89 | redo.isEnabled = canvas.canUndo(redo: true) | 117 | redo.isEnabled = canvas.canUndo(redo: true) |
| 90 | if navigationItem.rightBarButtonItems?.first !== done { | 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 | @objc private func leaving() { canvas.commitComposition() } | 184 | @objc private func leaving() { canvas.commitComposition() } |
| 96 | 185 | ||
| 97 | @objc private func sectionChanged(_ notification: Notification) { | 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,7 +10,7 @@ extension CanvasView { |
| 10 | /// `tap` and `doubletap` take points from the view's corner, `select` text offsets, | 10 | /// `tap` and `doubletap` take points from the view's corner, `select` text offsets, |
| 11 | /// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word` | 11 | /// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word` |
| 12 | /// selects a match, `title` edits the title, `picture` inserts a drawn picture, `selectall` | 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 | /// ends editing, `tree` saves the view hierarchy to | 14 | /// ends editing, `tree` saves the view hierarchy to |
| 15 | /// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no | 15 | /// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no |
| 16 | /// screenshot command. | 16 | /// screenshot command. |
| ... | @@ -54,6 +54,9 @@ extension CanvasView { | ... | @@ -54,6 +54,9 @@ extension CanvasView { |
| 54 | case "doubletap": | 54 | case "doubletap": |
| 55 | let values = argument.split(separator: ",").compactMap { Double($0) } | 55 | let values = argument.split(separator: ",").compactMap { Double($0) } |
| 56 | zoom(at: CGPoint(x: values[0] + contentOffset.x, y: values[1] + contentOffset.y)) | 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 | case "done": _ = resignFirstResponder() | 60 | case "done": _ = resignFirstResponder() |
| 58 | case "selectall": selectAll(nil) | 61 | case "selectall": selectAll(nil) |
| 59 | case "format": apply(UInt8(argument) ?? 0) | 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,7 +41,10 @@ the size of the edit, not the size of the page. |
| 41 | 41 | ||
| 42 | Undo lives in the editor, not in storage. Each history entry keeps the inverse | 42 | Undo lives in the editor, not in storage. Each history entry keeps the inverse |
| 43 | of the change it made. Undoing applies that inverse and emits its ops as a new | 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 | brings back the original identities, so internal links to those paragraphs | 48 | brings back the original identities, so internal links to those paragraphs |
| 46 | survive an undo. | 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,6 +90,22 @@ OneNote links it when it opens a page, without being stored as one. Ink draws st |
| 87 | coordinates. Page templates' background art is recreated as vector art and | 90 | coordinates. Page templates' background art is recreated as vector art and |
| 88 | recognised by the stored picture's hash. OneNote's bitmaps aren't shipped. | 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 | ## Content the editor doesn't understand | 109 | ## Content the editor doesn't understand |
| 91 | 110 | ||
| 92 | Nothing is lost for being unfamiliar. A paragraph the canvas can't draw | 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,9 +54,14 @@ the file grows): |
| 54 | file mean nothing until the header's transaction count says a transaction | 54 | file mean nothing until the header's transaction count says a transaction |
| 55 | holding them has committed. | 55 | holding them has committed. |
| 56 | 56 | ||
| 57 | External payloads (large pictures, attachments, recordings) can live beside | 57 | The format lets payloads (pictures, attachments, recordings) live beside the |
| 58 | the section as `.onebin` files in a `_onefiles` folder. The section refers to | 58 | section as `.onebin` files in a `_onefiles` folder, referred to by name. OneNote |
| 59 | them by name. | 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 | ## What "append one revision" means | 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,6 +58,13 @@ replayed and the open batch applied, rebuilt on open. The database runs in WAL |
| 58 | mode with full synchronous commits, and every setting is read back to check it | 58 | mode with full synchronous commits, and every setting is read back to check it |
| 59 | took. | 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 | A keystroke that publishes immediately costs three commits: the edit, the seal | 68 | A keystroke that publishes immediately costs three commits: the edit, the seal |
| 62 | (which also records the publication attempt), and the receipt. Each one is | 69 | (which also records the publication attempt), and the receipt. Each one is |
| 63 | ordered against something outside the cache. The edit must be durable before | 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,7 +116,8 @@ rounded or gradient rectangles, pen strokes and raster images. |
| 116 | - **Glyphs** are rasterized with Swash into an atlas, at quarter-pixel phases, | 116 | - **Glyphs** are rasterized with Swash into an atlas, at quarter-pixel phases, |
| 117 | so text placed at fractional positions stays crisp and doesn't shimmer as it | 117 | so text placed at fractional positions stays crisp and doesn't shimmer as it |
| 118 | moves. Icons and paths rasterize once per size and phase into the same | 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 | - **The renderer never measures text.** Text reaches it through a trait that | 121 | - **The renderer never measures text.** Text reaches it through a trait that |
| 121 | visits positioned glyph runs. Line metrics stay with whoever laid the text | 122 | visits positioned glyph runs. Line metrics stay with whoever laid the text |
| 122 | out, which is how the page keeps its Windows metrics while the chrome uses | 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,9 +129,10 @@ rounded or gradient rectangles, pen strokes and raster images. |
| 128 | or within shows through a row. A popup's contents clip to its rounded | 129 | or within shows through a row. A popup's contents clip to its rounded |
| 129 | outline, and one opening over a combo lays its rows out at their final | 130 | outline, and one opening over a combo lays its rows out at their final |
| 130 | width while only the outline widens. | 131 | width while only the outline widens. |
| 131 | - **Caches are bounded.** The glyph atlas and image cache have fixed budgets. | 132 | - **Caches are bounded.** The image cache has a fixed budget. A full glyph |
| 132 | Eviction keeps everything the current frame needs and rebuilds after | 133 | atlas is cleared and rebuilt with only what the current frame needs; where |
| 133 | pressure. Images are filtered in linear light with premultiplied alpha. | 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 | GPU readback tests pin the rasterization down: successive quarter-pixel | 137 | GPU readback tests pin the rasterization down: successive quarter-pixel |
| 136 | translations have to move the ink's centroid in quarter-pixel steps, and | 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,7 +10,8 @@ section's file-data store list, an embedded-file object declaring it by |
| 10 | identity and extension, and an attachment object with the file name, the | 10 | identity and extension, and an attachment object with the file name, the |
| 11 | 24 pt icon size and the flags every native attachment carries. `icon/` also | 11 | 24 pt icon size and the flags every native attachment carries. `icon/` also |
| 12 | embeds the native fixture's 724-byte icon PNG as the preview picture; | 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 | OneNote's integrity check refuses an attachment object without | 15 | OneNote's integrity check refuses an attachment object without |
| 15 | `0x1c001d61` (twenty bytes: 16, 1, zeros), so the writer always stores it. | 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,12 +2,16 @@ |
| 2 | 2 | ||
| 3 | `candidate` is `backgrounds_go_on_and_off_pages_that_have_content` in | 3 | `candidate` is `backgrounds_go_on_and_off_pages_that_have_content` in |
| 4 | `crates/snowbound/src/manage.rs` (`SNOWBOUND_BACKGROUND_EXPORT`), on a copy of | 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 | 5 | `rule-lines/native/notebook`: the Page Color menu's steps through the editor |
| 6 | Ivy's art (`manage::art_ops`), give "Wide" the art and take it away again, and colour | 6 | (`CanvasEditor::set_paper`), each stored as one edit. "Standard" takes Ivy's art; "Wide" |
| 7 | "SmallGrid" `0x00c8d8e8`, a COLORREF outside OneNote's menu. | 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 | 12 | `cold` is its fresh OneNote 2010 read: every ruled page keeps its lines; "Standard", |
| 10 | background image (`backgroundImage="true"`), "Wide" keeps its lines and no image, and | 13 | "MediumGrid" and "LargeGrid" show one Ivy background image (`backgroundImage="true"`), |
| 11 | "SmallGrid" reads back `color="#E8D8C8"` and shows it under its grid. `read/page-006.png` | 14 | "Wide" and "College" none; "SmallGrid" reads back `color="#E8D8C8"` under its grid, and |
| 12 | shows OneNote drawing the art over the rule lines. `tools/test_page_background.py` | 15 | "VeryLargeGrid" `#FDFDDD` under Sparks. `read/page-003.png` shows OneNote's opaque art |
| 13 | checks this without a VM. | 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,3 +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} | 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 | {"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} | 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 +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,7 +1,7 @@ |
| 1 | { | 1 | { |
| 2 | "powershell": "5.1.14409.1005", | 2 | "powershell": "5.1.14409.1005", |
| 3 | "schema": "xs2010", | 3 | "schema": "xs2010", |
| 4 | "hostname": "ONE-M6-5EF70348", | 4 | "hostname": "ONE-M6-0EA92EBC", |
| 5 | "cold": true, | 5 | "cold": true, |
| 6 | "onenote": "14.0.4763.1000" | 6 | "onenote": "14.0.4763.1000" |
| 7 | } | 7 | } |
corpus/page-background/cold/read/hierarchy.xml+1-1| ... | @@ -1,2 +1,2 @@ | ... | @@ -1,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "None - Microsoft OneNote", | 9 | "title": "SmallGrid - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "College - Microsoft OneNote", | 9 | "title": "Hidden - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "VeryLargeGrid - Microsoft OneNote", | 9 | "title": "LargeGrid - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,353 @@ |
| 1 | <?xml version="1.0"?> | 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 | 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</one:Data></one:Image></one:Page> |
corpus/page-background/cold/read/page-003.navigation.json+1-1| ... | @@ -6,7 +6,7 @@ | ... | @@ -6,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "MediumGrid - Microsoft OneNote", | 9 | "title": "Standard - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,353 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "Wide - Microsoft OneNote", | 9 | "title": "ColorRed - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "Narrow - Microsoft OneNote", | 9 | "title": "VeryLargeGrid - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,770 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "Standard - Microsoft OneNote", | 9 | "title": "MediumGrid - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,5 +1,5 @@ |
| 1 | <?xml version="1.0"?> | 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 | EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL | 3 | EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL |
| 4 | EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU | 4 | EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU |
| 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID | 5 | FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID |
corpus/page-background/cold/read/page-007.navigation.json+1-1| ... | @@ -6,7 +6,7 @@ | ... | @@ -6,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "LargeGrid - Microsoft OneNote", | 9 | "title": "Wide - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "ColorRed - Microsoft OneNote", | 9 | "title": "Narrow - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "SmallGrid - Microsoft OneNote", | 9 | "title": "None - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,7 +6,7 @@ |
| 6 | "h": 600, | 6 | "h": 600, |
| 7 | "error": null, | 7 | "error": null, |
| 8 | "win": { | 8 | "win": { |
| 9 | "title": "Hidden - Microsoft OneNote", | 9 | "title": "College - Microsoft OneNote", |
| 10 | "class": "Framework::CFrame", | 10 | "class": "Framework::CFrame", |
| 11 | "dialog": false | 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,2 +1,2 @@ |
| 1 | <?xml version="1.0"?> | 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,5 +1,5 @@ |
| 1 | { | 1 | { |
| 2 | "notebook": "/private/tmp/snowbound-page-background/candidate", | 2 | "notebook": "/private/tmp/snowbound-art-undo/candidate", |
| 3 | "expected_pages": 11, | 3 | "expected_pages": 11, |
| 4 | "author": null, | 4 | "author": null, |
| 5 | "author_timeout_seconds": 600, | 5 | "author_timeout_seconds": 600, |
corpus/page-background/cold/source.json+3-3| ... | @@ -7,8 +7,8 @@ | ... | @@ -7,8 +7,8 @@ |
| 7 | }, | 7 | }, |
| 8 | { | 8 | { |
| 9 | "path": "Rules.one", | 9 | "path": "Rules.one", |
| 10 | "bytes": 132704, | 10 | "bytes": 324016, |
| 11 | "sha256": "a513e56274ae794cf558da34dca7189225159d7eb6ab7d2256531fd7a2a55b44", | 11 | "sha256": "53c49a8bd35b4aee2ec840ef79fd7bdb6e6b4bc3cb3885d56cccbac145389a8b", |
| 12 | "mtime_ns": 1790671178905073494 | 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,6 +65,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 65 | PageObject::Image(image) => objects.push(json!({"kind": "image", "id": image.id, | 65 | PageObject::Image(image) => objects.push(json!({"kind": "image", "id": image.id, |
| 66 | "layout": image.layout, "bytes": image.bytes.as_ref().map(|b| b.len()), | 66 | "layout": image.layout, "bytes": image.bytes.as_ref().map(|b| b.len()), |
| 67 | "alt": image.alt, "background": image.background})), | 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 | PageObject::Ink(ink) => objects.push(json!({"kind": "ink", "id": ink.id, | 71 | PageObject::Ink(ink) => objects.push(json!({"kind": "ink", "id": ink.id, |
| 69 | "bounds": ink.bounds(), "strokes": ink.strokes.len(), "groups": ink.groups.len()})), | 72 | "bounds": ink.bounds(), "strokes": ink.strokes.len(), "groups": ink.groups.len()})), |
| 70 | PageObject::Unsupported(object) => objects.push(json!({"kind": "unsupported", | 73 | PageObject::Unsupported(object) => objects.push(json!({"kind": "unsupported", |
crates/canvas/src/editor.rs+616-76| ... | @@ -117,6 +117,9 @@ pub struct CanvasEditor { | ... | @@ -117,6 +117,9 @@ pub struct CanvasEditor { |
| 117 | /// What Select All widened to, holding until the selection is moved or set, and while the | 117 | /// What Select All widened to, holding until the selection is moved or set, and while the |
| 118 | /// focused outline and its selection stay as they were then. | 118 | /// focused outline and its selection stay as they were then. |
| 119 | whole: Option<(Whole, ExGuid, Selection)>, | 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 | /// Imported page state the editable content does not carry. | 125 | /// Imported page state the editable content does not carry. |
| ... | @@ -804,6 +807,12 @@ enum History { | ... | @@ -804,6 +807,12 @@ enum History { |
| 804 | index: usize, | 807 | index: usize, |
| 805 | image: Option<Box<onestore::page::Image>>, | 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 | Remove { | 816 | Remove { |
| 808 | outline: onestore::ExGuid, | 817 | outline: onestore::ExGuid, |
| 809 | focus: RestoreFocus, | 818 | focus: RestoreFocus, |
| ... | @@ -860,6 +869,7 @@ impl CanvasEditor { | ... | @@ -860,6 +869,7 @@ impl CanvasEditor { |
| 860 | ops: Ok(Vec::new()), | 869 | ops: Ok(Vec::new()), |
| 861 | stored: None, | 870 | stored: None, |
| 862 | whole: None, | 871 | whole: None, |
| 872 | typing: None, | ||
| 863 | }) | 873 | }) |
| 864 | } | 874 | } |
| 865 | 875 | ||
| ... | @@ -877,6 +887,7 @@ impl CanvasEditor { | ... | @@ -877,6 +887,7 @@ impl CanvasEditor { |
| 877 | .iter() | 887 | .iter() |
| 878 | .filter_map(|object| match object { | 888 | .filter_map(|object| match object { |
| 879 | page::Content::ReadOnly(object) => Some(object.rect()[2]), | 889 | page::Content::ReadOnly(object) => Some(object.rect()[2]), |
| 890 | page::Content::File { .. } => object.file().map(|(_, rect)| rect[2]), | ||
| 880 | page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]), | 891 | page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]), |
| 881 | _ => None, | 892 | _ => None, |
| 882 | }) | 893 | }) |
| ... | @@ -1047,7 +1058,9 @@ impl CanvasEditor { | ... | @@ -1047,7 +1058,9 @@ impl CanvasEditor { |
| 1047 | let reaches_one = |history: &History| match history { | 1058 | let reaches_one = |history: &History| match history { |
| 1048 | History::Date(_) => date_changed, | 1059 | History::Date(_) => date_changed, |
| 1049 | History::Paper { .. } => false, | 1060 | History::Paper { .. } => false, |
| 1050 | History::Image { .. } | History::Picture { .. } => objects_changed, | 1061 | History::Image { .. } | History::Picture { .. } | History::File { .. } => { |
| 1062 | objects_changed | ||
| 1063 | } | ||
| 1051 | History::Draft { outlines, .. } => changed.contains(&outlines[0].id), | 1064 | History::Draft { outlines, .. } => changed.contains(&outlines[0].id), |
| 1052 | History::Text { outline, change } => { | 1065 | History::Text { outline, change } => { |
| 1053 | changed.contains(outline) | 1066 | changed.contains(outline) |
| ... | @@ -1070,10 +1083,15 @@ impl CanvasEditor { | ... | @@ -1070,10 +1083,15 @@ impl CanvasEditor { |
| 1070 | History::Group { entries, .. } => entries.iter().any(reaches_one), | 1083 | History::Group { entries, .. } => entries.iter().any(reaches_one), |
| 1071 | history => reaches_one(history), | 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 | fresh.undo = std::mem::take(&mut self.undo) | 1090 | fresh.undo = std::mem::take(&mut self.undo) |
| 1074 | .into_iter() | 1091 | .into_iter() |
| 1075 | .filter(|history| !reaches(history)) | 1092 | .filter(|history| !reaches(history)) |
| 1076 | .collect(); | 1093 | .collect(); |
| 1094 | fresh.typing = run.map(|(outline, caret, _)| (outline, caret, fresh.undo.len())); | ||
| 1077 | fresh.redo = std::mem::take(&mut self.redo) | 1095 | fresh.redo = std::mem::take(&mut self.redo) |
| 1078 | .into_iter() | 1096 | .into_iter() |
| 1079 | .filter(|history| !reaches(history)) | 1097 | .filter(|history| !reaches(history)) |
| ... | @@ -1108,6 +1126,10 @@ impl CanvasEditor { | ... | @@ -1108,6 +1126,10 @@ impl CanvasEditor { |
| 1108 | objects.push(PageObject::Image(image.clone())); | 1126 | objects.push(PageObject::Image(image.clone())); |
| 1109 | continue; | 1127 | continue; |
| 1110 | } | 1128 | } |
| 1129 | page::Content::File { source, .. } => { | ||
| 1130 | objects.push(PageObject::Attachment(source.clone())); | ||
| 1131 | continue; | ||
| 1132 | } | ||
| 1111 | page::Content::Ink(ink) => { | 1133 | page::Content::Ink(ink) => { |
| 1112 | objects.push(PageObject::Ink(ink.clone())); | 1134 | objects.push(PageObject::Ink(ink.clone())); |
| 1113 | continue; | 1135 | continue; |
| ... | @@ -1423,6 +1445,7 @@ impl CanvasEditor { | ... | @@ -1423,6 +1445,7 @@ impl CanvasEditor { |
| 1423 | ops: Ok(Vec::new()), | 1445 | ops: Ok(Vec::new()), |
| 1424 | stored: None, | 1446 | stored: None, |
| 1425 | whole: None, | 1447 | whole: None, |
| 1448 | typing: None, | ||
| 1426 | }) | 1449 | }) |
| 1427 | } | 1450 | } |
| 1428 | 1451 | ||
| ... | @@ -1455,23 +1478,99 @@ impl CanvasEditor { | ... | @@ -1455,23 +1478,99 @@ impl CanvasEditor { |
| 1455 | Ok(true) | 1478 | Ok(true) |
| 1456 | } | 1479 | } |
| 1457 | 1480 | ||
| 1458 | /// Gives the page `color` (View, Page Color) and `rule_lines` (View, Rule Lines) as one | 1481 | /// Gives the page `color` (View, Page Color), `rule_lines` (View, Rule Lines) and, when |
| 1459 | /// undo step; false when it had them. | 1482 | /// given, `art` in place of its background pictures, as one undo step; false when it had |
| 1483 | /// them. | ||
| 1460 | pub fn set_paper( | 1484 | pub fn set_paper( |
| 1461 | &mut self, | 1485 | &mut self, |
| 1462 | color: Option<u32>, | 1486 | color: Option<u32>, |
| 1463 | rule_lines: Option<onestore::page::RuleLines>, | 1487 | rule_lines: Option<onestore::page::RuleLines>, |
| 1488 | art: Option<Vec<onestore::page::Image>>, | ||
| 1464 | ) -> bool { | 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 | return false; | 1506 | return false; |
| 1467 | } | 1507 | } |
| 1468 | self.finish_composition(); | 1508 | self.finish_composition(); |
| 1469 | let before = self.paper(color, rule_lines); | 1509 | let mut entries = Vec::new(); |
| 1470 | self.undo.push(before); | 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 | self.redo.clear(); | 1537 | self.redo.clear(); |
| 1472 | true | 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 | /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry | 1574 | /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry |
| 1476 | /// restoring what the page had. | 1575 | /// restoring what the page had. |
| 1477 | fn paper( | 1576 | fn paper( |
| ... | @@ -1754,9 +1853,12 @@ impl CanvasEditor { | ... | @@ -1754,9 +1853,12 @@ impl CanvasEditor { |
| 1754 | Ok(()) | 1853 | Ok(()) |
| 1755 | } | 1854 | } |
| 1756 | 1855 | ||
| 1757 | /// Origin and size of a picture the user can select; OneNote passes clicks through | 1856 | /// Origin and size of a picture or file on the page the user can select; OneNote passes |
| 1758 | /// backgrounds. | 1857 | /// clicks through backgrounds. |
| 1759 | pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> { | 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 | if let Some(image) = self.image(id) { | 1862 | if let Some(image) = self.image(id) { |
| 1761 | let layout = &image.layout; | 1863 | let layout = &image.layout; |
| 1762 | return Some(( | 1864 | return Some(( |
| ... | @@ -1776,20 +1878,50 @@ impl CanvasEditor { | ... | @@ -1776,20 +1878,50 @@ impl CanvasEditor { |
| 1776 | self.outline_picture(id).is_some() | 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, | 1881 | /// A picture or file held as a paragraph: its outline, the paragraph's container and index |
| 1780 | /// and the paragraph. | 1882 | /// there, and the paragraph. |
| 1781 | fn outline_picture( | 1883 | fn outline_picture( |
| 1782 | &self, | 1884 | &self, |
| 1783 | id: ExGuid, | 1885 | id: ExGuid, |
| 1784 | ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> { | 1886 | ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> { |
| 1785 | self.outlines.iter().find_map(|outline| { | 1887 | self.outlines.iter().find_map(|outline| { |
| 1786 | descendants(outline.document.nodes(), None).find_map(|(container, index, node)| { | 1888 | descendants(outline.document.nodes(), None).find_map(|(container, index, node)| { |
| 1787 | matches!(&node.content, onestore::page::ParagraphContent::Image(image) if image.id == id) | 1889 | holds(&node.content, id).then_some((outline.id, container, index, node)) |
| 1788 | .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 | /// Replaces a picture paragraph through the outline's text history. | 1925 | /// Replaces a picture paragraph through the outline's text history. |
| 1794 | fn edit_outline_picture( | 1926 | fn edit_outline_picture( |
| 1795 | &mut self, | 1927 | &mut self, |
| ... | @@ -1827,8 +1959,8 @@ impl CanvasEditor { | ... | @@ -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 | 1962 | /// Moves a picture or a file on the page, and resizes a picture when `size` differs from |
| 1831 | /// an outline's flow keeps its place. | 1963 | /// the stored size. A picture in an outline's flow keeps its place. |
| 1832 | pub fn place_image( | 1964 | pub fn place_image( |
| 1833 | &mut self, | 1965 | &mut self, |
| 1834 | engine: &mut TextEngine, | 1966 | engine: &mut TextEngine, |
| ... | @@ -1844,7 +1976,7 @@ impl CanvasEditor { | ... | @@ -1844,7 +1976,7 @@ impl CanvasEditor { |
| 1844 | if let Some((.., node)) = self.outline_picture(id) { | 1976 | if let Some((.., node)) = self.outline_picture(id) { |
| 1845 | let mut node = node.clone(); | 1977 | let mut node = node.clone(); |
| 1846 | let onestore::page::ParagraphContent::Image(image) = &mut node.content else { | 1978 | let onestore::page::ParagraphContent::Image(image) = &mut node.content else { |
| 1847 | unreachable!() | 1979 | return Ok(()); |
| 1848 | }; | 1980 | }; |
| 1849 | if crate::outline::image_size(image) == Some(size) { | 1981 | if crate::outline::image_size(image) == Some(size) { |
| 1850 | return Ok(()); | 1982 | return Ok(()); |
| ... | @@ -1853,6 +1985,25 @@ impl CanvasEditor { | ... | @@ -1853,6 +1985,25 @@ impl CanvasEditor { |
| 1853 | image.layout.width_set_by_user = Some(true); | 1985 | image.layout.width_set_by_user = Some(true); |
| 1854 | return self.edit_outline_picture(engine, id, Some(node)); | 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 | let image = self.image(id).ok_or(EditError::InvalidRange)?; | 2007 | let image = self.image(id).ok_or(EditError::InvalidRange)?; |
| 1857 | let mut layout = image.layout.clone(); | 2008 | let mut layout = image.layout.clone(); |
| 1858 | [layout.x, layout.y] = origin.map(Some); | 2009 | [layout.x, layout.y] = origin.map(Some); |
| ... | @@ -1897,9 +2048,7 @@ impl CanvasEditor { | ... | @@ -1897,9 +2048,7 @@ impl CanvasEditor { |
| 1897 | let mut passed = false; | 2048 | let mut passed = false; |
| 1898 | for (_, _, node) in descendants(source.document.nodes(), None) { | 2049 | for (_, _, node) in descendants(source.document.nodes(), None) { |
| 1899 | match &node.content { | 2050 | match &node.content { |
| 1900 | onestore::page::ParagraphContent::Image(image) if image.id == id => { | 2051 | content if holds(content, id) => passed = true, |
| 1901 | passed = true | ||
| 1902 | } | ||
| 1903 | onestore::page::ParagraphContent::Text(text) => { | 2052 | onestore::page::ParagraphContent::Text(text) => { |
| 1904 | let end = text.text.utf16_offset(text.text.text().len())?; | 2053 | let end = text.text.utf16_offset(text.text.text().len())?; |
| 1905 | if passed { | 2054 | if passed { |
| ... | @@ -1925,7 +2074,10 @@ impl CanvasEditor { | ... | @@ -1925,7 +2074,10 @@ impl CanvasEditor { |
| 1925 | let index = self | 2074 | let index = self |
| 1926 | .objects | 2075 | .objects |
| 1927 | .iter() | 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 | .ok_or(EditError::InvalidRange)?; | 2081 | .ok_or(EditError::InvalidRange)?; |
| 1930 | // An emptied outline keeps its slot for undo after leaving `outlines`. | 2082 | // An emptied outline keeps its slot for undo after leaving `outlines`. |
| 1931 | let editable = |object: &page::Content| match object { | 2083 | let editable = |object: &page::Content| match object { |
| ... | @@ -1989,10 +2141,95 @@ impl CanvasEditor { | ... | @@ -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 | pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> { | 2215 | pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> { |
| 1993 | if self.outline_picture(id).is_some() { | 2216 | if self.outline_picture(id).is_some() { |
| 1994 | return self.edit_outline_picture(engine, id, None); | 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 | let index = self | 2233 | let index = self |
| 1997 | .objects | 2234 | .objects |
| 1998 | .iter() | 2235 | .iter() |
| ... | @@ -2001,15 +2238,12 @@ impl CanvasEditor { | ... | @@ -2001,15 +2238,12 @@ impl CanvasEditor { |
| 2001 | }) | 2238 | }) |
| 2002 | .ok_or(EditError::InvalidRange)?; | 2239 | .ok_or(EditError::InvalidRange)?; |
| 2003 | self.finish_composition(); | 2240 | self.finish_composition(); |
| 2004 | let page::Content::Image(image) = self.objects.remove(index) else { | 2241 | let image = self.remove_picture(index); |
| 2005 | unreachable!() | ||
| 2006 | }; | ||
| 2007 | self.undo.push(History::Picture { | 2242 | self.undo.push(History::Picture { |
| 2008 | index, | 2243 | index, |
| 2009 | image: Some(Box::new(image)), | 2244 | image: Some(Box::new(image)), |
| 2010 | }); | 2245 | }); |
| 2011 | self.redo.clear(); | 2246 | self.redo.clear(); |
| 2012 | self.record(Ok(vec![PageOp::Delete { object: id }])); | ||
| 2013 | Ok(()) | 2247 | Ok(()) |
| 2014 | } | 2248 | } |
| 2015 | 2249 | ||
| ... | @@ -2227,6 +2461,55 @@ impl CanvasEditor { | ... | @@ -2227,6 +2461,55 @@ impl CanvasEditor { |
| 2227 | result | 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 | /// Removes the page selection's outlines, last first, but its last when `keep`, whose | 2513 | /// Removes the page selection's outlines, last first, but its last when `keep`, whose |
| 2231 | /// text stays selected to be replaced. Deleting the page selection leaves a caret where | 2514 | /// text stays selected to be replaced. Deleting the page selection leaves a caret where |
| 2232 | /// its first outline began, as OneNote 2010's does. | 2515 | /// its first outline began, as OneNote 2010's does. |
| ... | @@ -2375,6 +2658,7 @@ impl CanvasEditor { | ... | @@ -2375,6 +2658,7 @@ impl CanvasEditor { |
| 2375 | ) -> Result<(), EditorError> { | 2658 | ) -> Result<(), EditorError> { |
| 2376 | self.finish_composition(); | 2659 | self.finish_composition(); |
| 2377 | self.whole = None; | 2660 | self.whole = None; |
| 2661 | self.typing = None; | ||
| 2378 | if !extend && movement == Movement::Right && self.leave_equation()? { | 2662 | if !extend && movement == Movement::Right && self.leave_equation()? { |
| 2379 | return Ok(()); | 2663 | return Ok(()); |
| 2380 | } | 2664 | } |
| ... | @@ -2842,12 +3126,21 @@ impl CanvasEditor { | ... | @@ -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 | pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> { | 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 | if self.page_selected() { | 3140 | if self.page_selected() { |
| 2848 | return self.grouped(|editor| { | 3141 | return self.grouped(|editor| { |
| 2849 | editor.remove_page(engine, true)?; | 3142 | editor.remove_page(engine, true)?; |
| 2850 | editor.insert(engine, text) | 3143 | editor.insert_text(engine, text) |
| 2851 | }); | 3144 | }); |
| 2852 | } | 3145 | } |
| 2853 | self.leave_link_code()?; | 3146 | self.leave_link_code()?; |
| ... | @@ -3137,7 +3430,8 @@ impl CanvasEditor { | ... | @@ -3137,7 +3430,8 @@ impl CanvasEditor { |
| 3137 | let selection = self.active_outline().selection; | 3430 | let selection = self.active_outline().selection; |
| 3138 | let [anchor, focus] = selection.positions; | 3431 | let [anchor, focus] = selection.positions; |
| 3139 | let mut range = anchor.min(focus)..anchor.max(focus); | 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 | let outline = self.active_outline(); | 3435 | let outline = self.active_outline(); |
| 3142 | let paragraph = outline.paragraph_layout(focus.paragraph)?; | 3436 | let paragraph = outline.paragraph_layout(focus.paragraph)?; |
| 3143 | let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?; | 3437 | let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?; |
| ... | @@ -3213,7 +3507,11 @@ impl CanvasEditor { | ... | @@ -3213,7 +3507,11 @@ impl CanvasEditor { |
| 3213 | onestore::page::text::Affinity::Upstream, | 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 | pub fn delete_to( | 3517 | pub fn delete_to( |
| ... | @@ -3226,19 +3524,26 @@ impl CanvasEditor { | ... | @@ -3226,19 +3524,26 @@ impl CanvasEditor { |
| 3226 | } | 3524 | } |
| 3227 | self.finish_composition(); | 3525 | self.finish_composition(); |
| 3228 | let original = self.selection(); | 3526 | let original = self.selection(); |
| 3229 | let preferred_x = self.preferred_x; | 3527 | let (preferred_x, typing) = (self.preferred_x, self.typing); |
| 3230 | if original.positions[0] == original.positions[1] { | 3528 | let [caret, focus] = original.positions; |
| 3529 | let collapsed = caret == focus; | ||
| 3530 | if collapsed { | ||
| 3231 | let result = self.move_selection(engine, movement, true); | 3531 | let result = self.move_selection(engine, movement, true); |
| 3232 | if let Err(error) = result { | 3532 | if let Err(error) = result { |
| 3233 | self.active_outline_mut().selection = original; | 3533 | self.active_outline_mut().selection = original; |
| 3234 | self.preferred_x = preferred_x; | 3534 | (self.preferred_x, self.typing) = (preferred_x, typing); |
| 3235 | return Err(error); | 3535 | return Err(error); |
| 3236 | } | 3536 | } |
| 3237 | } | 3537 | } |
| 3238 | let [anchor, focus] = self.selection().positions; | 3538 | let [anchor, focus] = self.selection().positions; |
| 3239 | self.active_outline_mut().selection = original; | 3539 | self.active_outline_mut().selection = original; |
| 3240 | self.preferred_x = preferred_x; | 3540 | (self.preferred_x, self.typing) = (preferred_x, typing); |
| 3241 | self.delete_range(engine, anchor.min(focus)..anchor.max(focus)) | 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 | fn delete_range( | 3549 | fn delete_range( |
| ... | @@ -3326,6 +3631,7 @@ impl CanvasEditor { | ... | @@ -3326,6 +3631,7 @@ impl CanvasEditor { |
| 3326 | history: History, | 3631 | history: History, |
| 3327 | ) -> Result<History, (History, EditorError)> { | 3632 | ) -> Result<History, (History, EditorError)> { |
| 3328 | self.pending = None; | 3633 | self.pending = None; |
| 3634 | self.typing = None; | ||
| 3329 | let valid = match &history { | 3635 | let valid = match &history { |
| 3330 | History::Date(date) => self | 3636 | History::Date(date) => self |
| 3331 | .date | 3637 | .date |
| ... | @@ -3363,9 +3669,20 @@ impl CanvasEditor { | ... | @@ -3363,9 +3669,20 @@ impl CanvasEditor { |
| 3363 | index, | 3669 | index, |
| 3364 | image: Some(_), | 3670 | image: Some(_), |
| 3365 | } => *index <= self.objects.len(), | 3671 | } => *index <= self.objects.len(), |
| 3366 | History::Picture { index, image: None } => { | 3672 | History::Picture { index, image: None } => self |
| 3367 | matches!(self.objects.get(*index), Some(page::Content::Image(_))) | 3673 | .objects |
| 3368 | } | 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 | History::Remove { outline, focus } => { | 3686 | History::Remove { outline, focus } => { |
| 3370 | self.outlines.iter().any(|item| item.id == *outline) | 3687 | self.outlines.iter().any(|item| item.id == *outline) |
| 3371 | && match focus { | 3688 | && match focus { |
| ... | @@ -3584,24 +3901,20 @@ impl CanvasEditor { | ... | @@ -3584,24 +3901,20 @@ impl CanvasEditor { |
| 3584 | index, | 3901 | index, |
| 3585 | image: match image { | 3902 | image: match image { |
| 3586 | Some(image) => { | 3903 | Some(image) => { |
| 3587 | self.objects.insert(index, page::Content::Image(*image)); | 3904 | self.add_picture(index, *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)); | ||
| 3596 | None | 3905 | None |
| 3597 | } | 3906 | } |
| 3598 | None => match self.objects.remove(index) { | 3907 | None => Some(Box::new(self.remove_picture(index))), |
| 3599 | page::Content::Image(image) => { | 3908 | }, |
| 3600 | self.record(Ok(vec![PageOp::Delete { object: image.id }])); | 3909 | }, |
| 3601 | Some(Box::new(image)) | 3910 | History::File { index, file } => History::File { |
| 3602 | } | 3911 | index, |
| 3603 | _ => unreachable!(), | 3912 | file: match file { |
| 3604 | }, | 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 | History::Remove { outline, focus } => { | 3920 | History::Remove { outline, focus } => { |
| ... | @@ -3827,9 +4140,17 @@ impl CanvasEditor { | ... | @@ -3827,9 +4140,17 @@ impl CanvasEditor { |
| 3827 | .count() | 4140 | .count() |
| 3828 | .try_into() | 4141 | .try_into() |
| 3829 | .map_err(|_| EditError::TextTooLong)?; | 4142 | .map_err(|_| EditError::TextTooLong)?; |
| 3830 | self.compose(engine, text, end..end)?; | 4143 | let lines = text.contains('\n'); |
| 3831 | self.finish_composition(); | 4144 | let commit = |editor: &mut Self| { |
| 3832 | Ok(()) | 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 | } else { | 4154 | } else { |
| 3834 | self.insert(engine, &text) | 4155 | self.insert(engine, &text) |
| 3835 | } | 4156 | } |
| ... | @@ -4128,6 +4449,15 @@ impl CanvasEditor { | ... | @@ -4128,6 +4449,15 @@ impl CanvasEditor { |
| 4128 | /// Where `position` in `old` lies in `new`, the same outline changed elsewhere: in the same | 4449 | /// Where `position` in `old` lies in `new`, the same outline changed elsewhere: in the same |
| 4129 | /// paragraph, past what changed in its text when it lies after it; at the start of the | 4450 | /// paragraph, past what changed in its text when it lies after it; at the start of the |
| 4130 | /// paragraph now at its place when that one is gone. | 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 | fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition { | 4461 | fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition { |
| 4132 | let found = old.leaf(position.paragraph).and_then(|(_, _, node)| { | 4462 | let found = old.leaf(position.paragraph).and_then(|(_, _, node)| { |
| 4133 | let paragraph = new | 4463 | let paragraph = new |
| ... | @@ -5065,22 +5395,74 @@ mod tests { | ... | @@ -5065,22 +5395,74 @@ mod tests { |
| 5065 | editor.active_outline().shaped.paragraphs.len(), | 5395 | editor.active_outline().shaped.paragraphs.len(), |
| 5066 | fresh.paragraphs.len() | 5396 | fresh.paragraphs.len() |
| 5067 | ); | 5397 | ); |
| 5068 | editor.undo(&mut engine).unwrap(); | 5398 | // The committed composition and the typing after it are one run. |
| 5069 | assert_eq!( | ||
| 5070 | editor | ||
| 5071 | .active_outline() | ||
| 5072 | .document | ||
| 5073 | .paragraphs() | ||
| 5074 | .last() | ||
| 5075 | .unwrap() | ||
| 5076 | .text(), | ||
| 5077 | "Lasta" | ||
| 5078 | ); | ||
| 5079 | editor.undo(&mut engine).unwrap(); | 5399 | editor.undo(&mut engine).unwrap(); |
| 5080 | assert_eq!(editor.outlines()[0].document, source); | 5400 | assert_eq!(editor.outlines()[0].document, source); |
| 5081 | assert_eq!(editor.active_outline().document, draft); | 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 | #[test] | 5466 | #[test] |
| 5085 | fn table_horizontal_arrows_follow_reading_order() { | 5467 | fn table_horizontal_arrows_follow_reading_order() { |
| 5086 | let mut engine = TextEngine::default(); | 5468 | let mut engine = TextEngine::default(); |
| ... | @@ -5659,6 +6041,80 @@ mod tests { | ... | @@ -5659,6 +6041,80 @@ mod tests { |
| 5659 | assert_eq!(ids(&editor), [background_id]); | 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 | #[test] | 6118 | #[test] |
| 5663 | fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() { | 6119 | fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() { |
| 5664 | use onestore::page::{Image, Page, PageObject}; | 6120 | use onestore::page::{Image, Page, PageObject}; |
| ... | @@ -5858,6 +6314,94 @@ mod tests { | ... | @@ -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 | #[test] | 6405 | #[test] |
| 5862 | fn unknown_paragraphs_and_text_free_outlines_stay_on_the_page() { | 6406 | fn unknown_paragraphs_and_text_free_outlines_stay_on_the_page() { |
| 5863 | use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported}; | 6407 | use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported}; |
| ... | @@ -6405,17 +6949,6 @@ mod tests { | ... | @@ -6405,17 +6949,6 @@ mod tests { |
| 6405 | assert_eq!(editor.active_outline().shaped.size, expected.shaped.size); | 6949 | assert_eq!(editor.active_outline().shaped.size, expected.shaped.size); |
| 6406 | assert_eq!(editor.active_outline().document.nodes().len(), 2); | 6950 | assert_eq!(editor.active_outline().document.nodes().len(), 2); |
| 6407 | editor.undo(&mut engine).unwrap(); | 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 | assert_eq!(editor.outlines[0].document, source); | 6952 | assert_eq!(editor.outlines[0].document, source); |
| 6420 | assert_eq!(editor.active_outline().document.nodes().len(), 4); | 6953 | assert_eq!(editor.active_outline().document.nodes().len(), 4); |
| 6421 | } | 6954 | } |
| ... | @@ -7152,6 +7685,10 @@ mod tests { | ... | @@ -7152,6 +7685,10 @@ mod tests { |
| 7152 | let selection = editor.selection(); | 7685 | let selection = editor.selection(); |
| 7153 | editor.insert(&mut engine, "x").unwrap(); | 7686 | editor.insert(&mut engine, "x").unwrap(); |
| 7154 | assert_eq!(editor.outlines.len(), 1); | 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 | editor.delete(&mut engine, true).unwrap(); | 7692 | editor.delete(&mut engine, true).unwrap(); |
| 7156 | assert!(editor.outlines.is_empty()); | 7693 | assert!(editor.outlines.is_empty()); |
| 7157 | assert_eq!(editor.active_outline().document, spaces); | 7694 | assert_eq!(editor.active_outline().document, spaces); |
| ... | @@ -7368,6 +7905,9 @@ mod tests { | ... | @@ -7368,6 +7905,9 @@ mod tests { |
| 7368 | ) | 7905 | ) |
| 7369 | .unwrap(); | 7906 | .unwrap(); |
| 7370 | editor.insert(&mut engine, "One").unwrap(); | 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 | let source = editor.active_outline().snapshot(); | 7911 | let source = editor.active_outline().snapshot(); |
| 7372 | let selection = editor.selection(); | 7912 | let selection = editor.selection(); |
| 7373 | editor.insert(&mut engine, "!").unwrap(); | 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,13 +91,9 @@ fn open(engine: &mut TextEngine, summary: &str) -> CanvasEditor { |
| 91 | Some(label) => (label, true), | 91 | Some(label) => (label, true), |
| 92 | None => (tag, false), | 92 | None => (tag, false), |
| 93 | }; | 93 | }; |
| 94 | let tag = NoteTag::ALL | 94 | let defaults = NoteTag::defaults(); |
| 95 | .into_iter() | 95 | let place = defaults.iter().position(|tag| tag.label == label).unwrap(); |
| 96 | .find(|tag| { | 96 | let definition = define(label.into(), defaults[place].definition(place as u16)); |
| 97 | matches!(&tag.definition().kind, Kind::TagDefinition { label: Some(name), .. } if name == label) | ||
| 98 | }) | ||
| 99 | .unwrap(); | ||
| 100 | let definition = define(label.into(), tag.definition()); | ||
| 101 | node.text_mut().unwrap().tags.push(Tag { | 97 | node.text_mut().unwrap().tags.push(Tag { |
| 102 | definition: Some(definition), | 98 | definition: Some(definition), |
| 103 | action_type: None, | 99 | action_type: None, |
| ... | @@ -146,9 +142,12 @@ fn render(editor: &CanvasEditor) -> String { | ... | @@ -146,9 +142,12 @@ fn render(editor: &CanvasEditor) -> String { |
| 146 | }; | 142 | }; |
| 147 | let checked = if tag.status & 1 == 1 { "[x]" } else { "" }; | 143 | let checked = if tag.status & 1 == 1 { "[x]" } else { "" }; |
| 148 | ( | 144 | ( |
| 149 | NoteTag::ALL | 145 | NoteTag::of(kind).and_then(|(tag, action_type)| { |
| 150 | .iter() | 146 | NoteTag::defaults() |
| 151 | .position(|known| known.definition().kind == *kind), | 147 | .iter() |
| 148 | .position(|known| *known == tag) | ||
| 149 | .filter(|place| *place == usize::from(action_type)) | ||
| 150 | }), | ||
| 152 | format!("{}{checked}", label.as_deref().unwrap()), | 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,6 +5,7 @@ use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE}; |
| 5 | use onestore::document::{Format, Kind, Tag}; | 5 | use onestore::document::{Format, Kind, Tag}; |
| 6 | use onestore::page::ParagraphContent; | 6 | use onestore::page::ParagraphContent; |
| 7 | use onestore::page::text::new_id; | 7 | use onestore::page::text::new_id; |
| 8 | use serde::{Deserialize, Serialize}; | ||
| 8 | 9 | ||
| 9 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. | 10 | /// The COLORREF OneNote stores for automatic colour, which overrides a style's colour. |
| 10 | const AUTOMATIC: u32 = 0xff00_0000; | 11 | const AUTOMATIC: u32 = 0xff00_0000; |
| ... | @@ -32,7 +33,9 @@ pub enum Formatting { | ... | @@ -32,7 +33,9 @@ pub enum Formatting { |
| 32 | Indent, | 33 | Indent, |
| 33 | Outdent, | 34 | Outdent, |
| 34 | Align(Alignment), | 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 | /// Removes every note tag from the selected paragraphs. | 39 | /// Removes every note tag from the selected paragraphs. |
| 37 | RemoveTags, | 40 | RemoveTags, |
| 38 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. | 41 | /// Checks the selected paragraphs' check boxes, or clears them when all are checked. |
| ... | @@ -161,40 +164,102 @@ impl ListStyle { | ... | @@ -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 | 167 | /// A tag of the user's list as the Customize Tags dialog edits it: what its definition |
| 165 | /// definitions (`corpus/structural-probe/tag-gallery.one`); declared in gallery order, their | 168 | /// stores besides its place in the list, which the page stores as its action type. |
| 166 | /// action types. | 169 | #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] |
| 167 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 170 | pub struct NoteTag { |
| 168 | pub enum NoteTag { | 171 | pub label: String, |
| 169 | ToDo, | 172 | /// MS-ONE's NoteTagShape; 0 has no symbol. |
| 170 | Important, | 173 | pub shape: u16, |
| 171 | Question, | 174 | /// COLORREFs; none leaves the text's own. |
| 172 | RememberForLater, | 175 | pub color: Option<u32>, |
| 173 | Definition, | 176 | pub highlight: Option<u32>, |
| 174 | Highlight, | 177 | } |
| 175 | Contact, | 178 | |
| 176 | Address, | 179 | /// OneNote 2010's tag list before it is customized, the first nine on Ctrl+1 to Ctrl+9 |
| 177 | PhoneNumber, | 180 | /// (`corpus/structural-probe/tag-gallery.one`): names, symbols and highlights. |
| 178 | WebSiteToVisit, | 181 | const DEFAULT_TAGS: [(&str, u16, Option<u32>); 29] = [ |
| 179 | Idea, | 182 | ("To Do", CHECKBOX, None), |
| 180 | Password, | 183 | ("Important", 13, None), |
| 181 | Critical, | 184 | ("Question", 15, None), |
| 182 | ProjectA, | 185 | ("Remember for later", 0, Some(0x0000_ffff)), |
| 183 | ProjectB, | 186 | ("Definition", 0, Some(0x0000_ff00)), |
| 184 | MovieToSee, | 187 | ("Highlight", 136, None), |
| 185 | BookToRead, | 188 | ("Contact", 118, None), |
| 186 | MusicToListenTo, | 189 | ("Address", 23, None), |
| 187 | SourceForArticle, | 190 | ("Phone number", 18, None), |
| 188 | RememberForBlog, | 191 | ("Web site to visit", 125, None), |
| 189 | DiscussWithPersonA, | 192 | ("Idea", 21, None), |
| 190 | DiscussWithPersonB, | 193 | ("Password", 131, None), |
| 191 | DiscussWithManager, | 194 | ("Critical", 17, None), |
| 192 | SendInEmail, | 195 | ("Project A", 100, None), |
| 193 | ScheduleMeeting, | 196 | ("Project B", 101, None), |
| 194 | CallBack, | 197 | ("Movie to see", 122, None), |
| 195 | ToDoPriority1, | 198 | ("Book to read", 132, None), |
| 196 | ToDoPriority2, | 199 | ("Music to listen to", 121, None), |
| 197 | ClientRequest, | 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 | /// What the selection shows on the toolbar. | 265 | /// What the selection shows on the toolbar. |
| ... | @@ -212,8 +277,8 @@ pub struct FormatState { | ... | @@ -212,8 +277,8 @@ pub struct FormatState { |
| 212 | pub numbering: bool, | 277 | pub numbering: bool, |
| 213 | /// The library style every selected paragraph's list shares. | 278 | /// The library style every selected paragraph's list shares. |
| 214 | pub list: Option<ListStyle>, | 279 | pub list: Option<ListStyle>, |
| 215 | /// Tags every selected paragraph has. | 280 | /// Tags every selected paragraph has, with their action types. |
| 216 | pub tags: Vec<NoteTag>, | 281 | pub tags: Vec<(NoteTag, u16)>, |
| 217 | } | 282 | } |
| 218 | 283 | ||
| 219 | impl Toggle { | 284 | impl Toggle { |
| ... | @@ -249,133 +314,6 @@ 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 | /// Where a selection starts and ends, as text leaf identity and byte offset. | 317 | /// Where a selection starts and ends, as text leaf identity and byte offset. |
| 380 | type Ends = [(ExGuid, usize); 2]; | 318 | type Ends = [(ExGuid, usize); 2]; |
| 381 | 319 | ||
| ... | @@ -664,15 +602,19 @@ impl CanvasEditor { | ... | @@ -664,15 +602,19 @@ impl CanvasEditor { |
| 664 | bullets: list(Formatting::Bullets), | 602 | bullets: list(Formatting::Bullets), |
| 665 | numbering: list(Formatting::Numbering), | 603 | numbering: list(Formatting::Numbering), |
| 666 | list: common(paragraphs.iter().map(|node| self.list_style(node))), | 604 | list: common(paragraphs.iter().map(|node| self.list_style(node))), |
| 667 | tags: NoteTag::ALL | 605 | tags: paragraphs.first().map_or_else(Vec::new, |first| { |
| 668 | .into_iter() | 606 | let mut shared: Vec<_> = tags(first) |
| 669 | .filter(|tag| { | 607 | .filter_map(|tag| NoteTag::of(self.tag_kind(tag)?)) |
| 670 | let kind = tag.definition().kind; | 608 | .filter(|(tag, action_type)| { |
| 671 | paragraphs | 609 | let kind = tag.definition(*action_type).kind; |
| 672 | .iter() | 610 | paragraphs |
| 673 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) | 611 | .iter() |
| 674 | }) | 612 | .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind))) |
| 675 | .collect(), | 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,7 +644,7 @@ impl CanvasEditor { |
| 702 | | Formatting::Bullets | 644 | | Formatting::Bullets |
| 703 | | Formatting::Numbering | 645 | | Formatting::Numbering |
| 704 | | Formatting::List(_) | 646 | | Formatting::List(_) |
| 705 | | Formatting::Tag(_) | 647 | | Formatting::Tag(..) |
| 706 | | Formatting::RemoveTags | 648 | | Formatting::RemoveTags |
| 707 | | Formatting::Check | 649 | | Formatting::Check |
| 708 | if title => | 650 | if title => |
| ... | @@ -783,14 +725,8 @@ impl CanvasEditor { | ... | @@ -783,14 +725,8 @@ impl CanvasEditor { |
| 783 | } | 725 | } |
| 784 | }); | 726 | }); |
| 785 | } | 727 | } |
| 786 | Formatting::Tag(tag) => { | 728 | Formatting::Tag(tag, action_type) => { |
| 787 | let definition = tag.definition(); | 729 | let definition = tag.definition(*action_type); |
| 788 | let Kind::TagDefinition { | ||
| 789 | action_type, shape, .. | ||
| 790 | } = definition.kind | ||
| 791 | else { | ||
| 792 | unreachable!() | ||
| 793 | }; | ||
| 794 | let existing = self | 730 | let existing = self |
| 795 | .definitions | 731 | .definitions |
| 796 | .iter() | 732 | .iter() |
| ... | @@ -821,12 +757,12 @@ impl CanvasEditor { | ... | @@ -821,12 +757,12 @@ impl CanvasEditor { |
| 821 | Some(kind) if remove => *kind != definition.kind, | 757 | Some(kind) if remove => *kind != definition.kind, |
| 822 | Some(Kind::TagDefinition { | 758 | Some(Kind::TagDefinition { |
| 823 | action_type: other, .. | 759 | action_type: other, .. |
| 824 | }) => *other != action_type, | 760 | }) => *other != Some(*action_type), |
| 825 | _ => true, | 761 | _ => true, |
| 826 | }); | 762 | }); |
| 827 | } | 763 | } |
| 828 | if !remove { | 764 | if !remove { |
| 829 | let checkable = checkable(shape); | 765 | let checkable = crate::outline::checkable(tag.shape); |
| 830 | // Stored newest first. | 766 | // Stored newest first. |
| 831 | text.tags.insert( | 767 | text.tags.insert( |
| 832 | 0, | 768 | 0, |
| ... | @@ -967,7 +903,7 @@ impl CanvasEditor { | ... | @@ -967,7 +903,7 @@ impl CanvasEditor { |
| 967 | Formatting::Toggle(toggle) => state.toggles.contains(toggle), | 903 | Formatting::Toggle(toggle) => state.toggles.contains(toggle), |
| 968 | Formatting::Bullets => state.bullets, | 904 | Formatting::Bullets => state.bullets, |
| 969 | Formatting::Numbering => state.numbering, | 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 | _ => false, | 907 | _ => false, |
| 972 | }; | 908 | }; |
| 973 | let mut outlines = Vec::new(); | 909 | let mut outlines = Vec::new(); |
| ... | @@ -1038,7 +974,8 @@ impl CanvasEditor { | ... | @@ -1038,7 +974,8 @@ impl CanvasEditor { |
| 1038 | let checkable = |tag: &Tag| { | 974 | let checkable = |tag: &Tag| { |
| 1039 | matches!( | 975 | matches!( |
| 1040 | self.tag_kind(tag), | 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 | let checked = self::covered(replacement, ends) | 981 | let checked = self::covered(replacement, ends) |
| ... | @@ -1823,7 +1760,10 @@ mod tests { | ... | @@ -1823,7 +1760,10 @@ mod tests { |
| 1823 | let mut editor = plain(&mut engine, &["task", "other"]); | 1760 | let mut editor = plain(&mut engine, &["task", "other"]); |
| 1824 | editor.select([at(0, 0), at(1, 5)].into()).unwrap(); | 1761 | editor.select([at(0, 0), at(1, 5)].into()).unwrap(); |
| 1825 | editor | 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 | .unwrap(); | 1767 | .unwrap(); |
| 1828 | editor.select([at(1, 2); 2].into()).unwrap(); | 1768 | editor.select([at(1, 2); 2].into()).unwrap(); |
| 1829 | let outline = editor.active_outline().id; | 1769 | let outline = editor.active_outline().id; |
| ... | @@ -1886,27 +1826,33 @@ mod tests { | ... | @@ -1886,27 +1826,33 @@ mod tests { |
| 1886 | let original = editor.active_outline().document.clone(); | 1826 | let original = editor.active_outline().document.clone(); |
| 1887 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); | 1827 | editor.select([at(0, 1), at(1, 2)].into()).unwrap(); |
| 1888 | editor | 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 | .unwrap(); | 1833 | .unwrap(); |
| 1891 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]); | 1834 | assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]); |
| 1892 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]); | 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 | let icon = | 1840 | let icon = |
| 1895 | |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon; | 1841 | |editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon; |
| 1896 | assert_eq!( | 1842 | assert_eq!( |
| 1897 | icon(&editor), | 1843 | icon(&editor), |
| 1898 | crate::outline::TagIcon::CheckBox { | 1844 | crate::outline::TagIcon::Symbol { |
| 1899 | checked: false, | 1845 | shape: 3, |
| 1900 | mark: None | 1846 | checked: false |
| 1901 | } | 1847 | } |
| 1902 | ); | 1848 | ); |
| 1903 | editor.format(&mut engine, Formatting::Check).unwrap(); | 1849 | editor.format(&mut engine, Formatting::Check).unwrap(); |
| 1904 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); | 1850 | assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]); |
| 1905 | assert_eq!( | 1851 | assert_eq!( |
| 1906 | icon(&editor), | 1852 | icon(&editor), |
| 1907 | crate::outline::TagIcon::CheckBox { | 1853 | crate::outline::TagIcon::Symbol { |
| 1908 | checked: true, | 1854 | shape: 3, |
| 1909 | mark: None | 1855 | checked: true |
| 1910 | } | 1856 | } |
| 1911 | ); | 1857 | ); |
| 1912 | editor.select([at(1, 0); 2].into()).unwrap(); | 1858 | editor.select([at(1, 0); 2].into()).unwrap(); |
| ... | @@ -1918,27 +1864,39 @@ mod tests { | ... | @@ -1918,27 +1864,39 @@ mod tests { |
| 1918 | 1864 | ||
| 1919 | editor.select([at(1, 0), at(2, 5)].into()).unwrap(); | 1865 | editor.select([at(1, 0), at(2, 5)].into()).unwrap(); |
| 1920 | editor | 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 | .unwrap(); | 1871 | .unwrap(); |
| 1923 | assert_eq!( | 1872 | assert_eq!( |
| 1924 | text_tags(&editor, 1), | 1873 | text_tags(&editor, 1), |
| 1925 | [("Question".into(), 1, true), ("To Do".into(), 1, true)] | 1874 | [("Question".into(), 1, true), ("To Do".into(), 1, true)] |
| 1926 | ); | 1875 | ); |
| 1927 | assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]); | 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 | let questions = editor | 1881 | let questions = editor |
| 1930 | .definitions | 1882 | .definitions |
| 1931 | .values() | 1883 | .values() |
| 1932 | .filter(|definition| **definition == NoteTag::Question.definition()) | 1884 | .filter(|definition| **definition == NoteTag::defaults()[2].definition(2)) |
| 1933 | .count(); | 1885 | .count(); |
| 1934 | assert_eq!(questions, 1); | 1886 | assert_eq!(questions, 1); |
| 1935 | editor | 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 | .unwrap(); | 1892 | .unwrap(); |
| 1938 | assert_eq!(text_tags(&editor, 2), []); | 1893 | assert_eq!(text_tags(&editor, 2), []); |
| 1939 | editor.select([at(0, 0); 2].into()).unwrap(); | 1894 | editor.select([at(0, 0); 2].into()).unwrap(); |
| 1940 | editor | 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 | .unwrap(); | 1900 | .unwrap(); |
| 1943 | assert_eq!(text_tags(&editor, 0), []); | 1901 | assert_eq!(text_tags(&editor, 0), []); |
| 1944 | for _ in 0..7 { | 1902 | for _ in 0..7 { |
| ... | @@ -1961,44 +1919,35 @@ mod tests { | ... | @@ -1961,44 +1919,35 @@ mod tests { |
| 1961 | .values() | 1919 | .values() |
| 1962 | .filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. })) | 1920 | .filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. })) |
| 1963 | .collect::<Vec<_>>(); | 1921 | .collect::<Vec<_>>(); |
| 1964 | assert_eq!(stored.len(), NoteTag::ALL.len()); | 1922 | let defaults = NoteTag::defaults(); |
| 1965 | for tag in NoteTag::ALL { | 1923 | assert_eq!(stored.len(), defaults.len()); |
| 1966 | assert!(stored.contains(&&tag.definition()), "{tag:?}"); | 1924 | for (place, tag) in defaults.iter().enumerate() { |
| 1925 | assert!(stored.contains(&&tag.definition(place as u16)), "{tag:?}"); | ||
| 1967 | } | 1926 | } |
| 1968 | let mut engine = TextEngine::default(); | 1927 | let mut engine = TextEngine::default(); |
| 1969 | let mut editor = plain(&mut engine, &["tagged"]); | 1928 | let mut editor = plain(&mut engine, &["tagged"]); |
| 1970 | for tag in NoteTag::ALL { | 1929 | for (place, tag) in defaults.iter().enumerate() { |
| 1971 | editor.format(&mut engine, Formatting::Tag(tag)).unwrap(); | 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 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; | 1935 | let paragraph = &editor.active_outline().shaped.paragraphs[0]; |
| 1975 | use crate::outline::TagIcon; | 1936 | let shapes = paragraph |
| 1976 | let icons = paragraph | ||
| 1977 | .tags | 1937 | .tags |
| 1978 | .iter() | 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 | .collect::<Vec<_>>(); | 1943 | .collect::<Vec<_>>(); |
| 1981 | assert_eq!(icons.len(), NoteTag::ALL.len() - 2); | 1944 | assert_eq!(shapes.len(), defaults.len() - 2); |
| 1982 | assert_eq!( | 1945 | assert_eq!(shapes[..7], [3, 13, 15, 136, 118, 23, 18]); |
| 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 | ); | ||
| 1997 | // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back, | 1946 | // As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back, |
| 1998 | // the priorities and Client request. | 1947 | // the priorities and Client request. |
| 1999 | let checkable = NoteTag::ALL | 1948 | let checkable = defaults |
| 2000 | .into_iter() | 1949 | .iter() |
| 2001 | .filter(|tag| checkable(Some(tag.shape()))) | 1950 | .filter(|tag| crate::outline::checkable(tag.shape)) |
| 2002 | .count(); | 1951 | .count(); |
| 2003 | assert_eq!(checkable, 9); | 1952 | assert_eq!(checkable, 9); |
| 2004 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. | 1953 | // Remember for later and Definition draw no symbol; the newer one's green marks the text. |
| ... | @@ -2011,6 +1960,46 @@ mod tests { | ... | @@ -2011,6 +1960,46 @@ mod tests { |
| 2011 | assert!(paragraph.text.backgrounds().next().is_some()); | 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 | #[test] | 2003 | #[test] |
| 2015 | fn commands_reach_every_cell_a_selection_crosses() { | 2004 | fn commands_reach_every_cell_a_selection_crosses() { |
| 2016 | let mut engine = TextEngine::default(); | 2005 | let mut engine = TextEngine::default(); |
| ... | @@ -2049,7 +2038,10 @@ mod tests { | ... | @@ -2049,7 +2038,10 @@ mod tests { |
| 2049 | editor.format(&mut engine, Formatting::Numbering).unwrap(); | 2038 | editor.format(&mut engine, Formatting::Numbering).unwrap(); |
| 2050 | assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]); | 2039 | assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]); |
| 2051 | editor | 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 | .unwrap(); | 2045 | .unwrap(); |
| 2054 | assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1); | 2046 | assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1); |
| 2055 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); | 2047 | assert_eq!(editor.selection(), Selection::from([at(1, 0); 2])); |
| ... | @@ -2110,8 +2102,8 @@ mod tests { | ... | @@ -2110,8 +2102,8 @@ mod tests { |
| 2110 | Formatting::Highlight(Some(0x0000_ffff)), | 2102 | Formatting::Highlight(Some(0x0000_ffff)), |
| 2111 | Formatting::Align(Alignment::Center), | 2103 | Formatting::Align(Alignment::Center), |
| 2112 | Formatting::Numbering, | 2104 | Formatting::Numbering, |
| 2113 | Formatting::Tag(NoteTag::ToDo), | 2105 | Formatting::Tag(NoteTag::defaults()[0].clone(), 0), |
| 2114 | Formatting::Tag(NoteTag::Question), | 2106 | Formatting::Tag(NoteTag::defaults()[2].clone(), 2), |
| 2115 | Formatting::Check, | 2107 | Formatting::Check, |
| 2116 | ] { | 2108 | ] { |
| 2117 | editor.format(&mut engine, command).unwrap(); | 2109 | editor.format(&mut engine, command).unwrap(); |
| ... | @@ -2136,7 +2128,10 @@ mod tests { | ... | @@ -2136,7 +2128,10 @@ mod tests { |
| 2136 | bullets: false, | 2128 | bullets: false, |
| 2137 | numbering: true, | 2129 | numbering: true, |
| 2138 | list: Some(ListStyle::NUMBER), | 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 | // Format Painter, a gallery tag and an inserted table write as well. | 2137 | // Format Painter, a gallery tag and an inserted table write as well. |
| ... | @@ -2155,7 +2150,10 @@ mod tests { | ... | @@ -2155,7 +2150,10 @@ mod tests { |
| 2155 | ) | 2150 | ) |
| 2156 | ); | 2151 | ); |
| 2157 | editor | 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 | .unwrap(); | 2157 | .unwrap(); |
| 2160 | editor.select([at(0, 1); 2].into()).unwrap(); | 2158 | editor.select([at(0, 1); 2].into()).unwrap(); |
| 2161 | editor.insert_table(&mut engine, 2, 3).unwrap(); | 2159 | editor.insert_table(&mut engine, 2, 3).unwrap(); |
crates/canvas/src/editor/ops.rs+4| ... | @@ -330,6 +330,9 @@ impl CanvasEditor { | ... | @@ -330,6 +330,9 @@ impl CanvasEditor { |
| 330 | page::Content::Ink(ink) if ink.id == placement.id => { | 330 | page::Content::Ink(ink) if ink.id == placement.id => { |
| 331 | return layout_ops(ink.id, &old, &moved(&ink.layout)); | 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 | page::Content::Outline { source, .. } if source.id == placement.id => { | 336 | page::Content::Outline { source, .. } if source.id == placement.id => { |
| 334 | return layout_ops(source.id, &old, &moved(&source.layout)); | 337 | return layout_ops(source.id, &old, &moved(&source.layout)); |
| 335 | } | 338 | } |
| ... | @@ -434,6 +437,7 @@ impl CanvasEditor { | ... | @@ -434,6 +437,7 @@ impl CanvasEditor { |
| 434 | page::Content::Outline { source, .. } => source.id, | 437 | page::Content::Outline { source, .. } => source.id, |
| 435 | page::Content::Date { .. } => continue, | 438 | page::Content::Date { .. } => continue, |
| 436 | page::Content::Image(image) => image.id, | 439 | page::Content::Image(image) => image.id, |
| 440 | page::Content::File { source, .. } => source.id, | ||
| 437 | page::Content::Ink(ink) => ink.id, | 441 | page::Content::Ink(ink) => ink.id, |
| 438 | page::Content::ReadOnly(object) | 442 | page::Content::ReadOnly(object) |
| 439 | if matches!(object.source, PageObject::Title(_)) => | 443 | if matches!(object.source, PageObject::Title(_)) => |
crates/canvas/src/editor/page.rs+43-1| ... | @@ -5,7 +5,7 @@ use crate::{ | ... | @@ -5,7 +5,7 @@ use crate::{ |
| 5 | outline::{Arrange, OutlineLayout}, | 5 | outline::{Arrange, OutlineLayout}, |
| 6 | }; | 6 | }; |
| 7 | use onestore::page::text::Paragraph; | 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 | use std::collections::BTreeMap; | 9 | use std::collections::BTreeMap; |
| 10 | 10 | ||
| 11 | /// A title object's own state, plus the child origins `build` replaces with page coordinates. | 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,6 +33,11 @@ pub(crate) enum Content { |
| 33 | below_title: Option<onestore::ExGuid>, | 33 | below_title: Option<onestore::ExGuid>, |
| 34 | }, | 34 | }, |
| 35 | Image(Image), | 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 | Ink(Ink), | 41 | Ink(Ink), |
| 37 | Editable(onestore::ExGuid), | 42 | Editable(onestore::ExGuid), |
| 38 | ReadOnly(Box<ReadOnlyObject>), | 43 | ReadOnly(Box<ReadOnlyObject>), |
| ... | @@ -60,22 +65,46 @@ impl Content { | ... | @@ -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 | pub(super) fn layout(&self) -> Option<(onestore::ExGuid, &onestore::document::Layout)> { | 80 | pub(super) fn layout(&self) -> Option<(onestore::ExGuid, &onestore::document::Layout)> { |
| 64 | match self { | 81 | match self { |
| 65 | Self::Outline { source, .. } => Some((source.id, &source.layout)), | 82 | Self::Outline { source, .. } => Some((source.id, &source.layout)), |
| 66 | Self::Image(source) => Some((source.id, &source.layout)), | 83 | Self::Image(source) => Some((source.id, &source.layout)), |
| 84 | Self::File { source, .. } => Some((source.id, &source.layout)), | ||
| 67 | Self::Ink(source) => Some((source.id, &source.layout)), | 85 | Self::Ink(source) => Some((source.id, &source.layout)), |
| 68 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout())), | 86 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout())), |
| 69 | Self::Date { .. } | Self::Editable(_) => None, | 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 | pub(super) fn layout_mut( | 101 | pub(super) fn layout_mut( |
| 74 | &mut self, | 102 | &mut self, |
| 75 | ) -> Option<(onestore::ExGuid, &mut onestore::document::Layout)> { | 103 | ) -> Option<(onestore::ExGuid, &mut onestore::document::Layout)> { |
| 76 | match self { | 104 | match self { |
| 77 | Self::Outline { source, .. } => Some((source.id, &mut source.layout)), | 105 | Self::Outline { source, .. } => Some((source.id, &mut source.layout)), |
| 78 | Self::Image(source) => Some((source.id, &mut source.layout)), | 106 | Self::Image(source) => Some((source.id, &mut source.layout)), |
| 107 | Self::File { source, .. } => Some((source.id, &mut source.layout)), | ||
| 79 | Self::Ink(source) => Some((source.id, &mut source.layout)), | 108 | Self::Ink(source) => Some((source.id, &mut source.layout)), |
| 80 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())), | 109 | Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())), |
| 81 | Self::Date { .. } | Self::Editable(_) => None, | 110 | Self::Date { .. } | Self::Editable(_) => None, |
| ... | @@ -332,6 +361,19 @@ pub(crate) fn build( | ... | @@ -332,6 +361,19 @@ pub(crate) fn build( |
| 332 | }; | 361 | }; |
| 333 | objects.push(Content::Image(source)); | 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 | PageObject::Ink(ink) => { | 377 | PageObject::Ink(ink) => { |
| 336 | if ink_bounds(ink).is_some_and(|b| b.iter().any(|v| !v.is_finite())) { | 378 | if ink_bounds(ink).is_some_and(|b| b.iter().any(|v| !v.is_finite())) { |
| 337 | return Err(EditorError::InvalidGeometry); | 379 | return Err(EditorError::InvalidGeometry); |
crates/canvas/src/editor/space.rs+2-1| ... | @@ -5,7 +5,7 @@ use super::*; | ... | @@ -5,7 +5,7 @@ use super::*; |
| 5 | 5 | ||
| 6 | /// Something Insert Space moves. | 6 | /// Something Insert Space moves. |
| 7 | enum Piece { | 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 | Object(ExGuid), | 9 | Object(ExGuid), |
| 10 | /// The root nodes of a body outline from `at` on. | 10 | /// The root nodes of a body outline from `at` on. |
| 11 | Tail { outline: ExGuid, at: usize }, | 11 | Tail { outline: ExGuid, at: usize }, |
| ... | @@ -81,6 +81,7 @@ impl CanvasEditor { | ... | @@ -81,6 +81,7 @@ impl CanvasEditor { |
| 81 | ]; | 81 | ]; |
| 82 | (image.id, rect) | 82 | (image.id, rect) |
| 83 | } | 83 | } |
| 84 | page::Content::File { source, .. } => (source.id, content.file().unwrap().1), | ||
| 84 | page::Content::Ink(ink) => match page::ink_bounds(ink) { | 85 | page::Content::Ink(ink) => match page::ink_bounds(ink) { |
| 85 | Some(rect) => (ink.id, rect), | 86 | Some(rect) => (ink.id, rect), |
| 86 | None => continue, | 87 | None => continue, |
crates/canvas/src/gpu/mod.rs+206-126| ... | @@ -4,7 +4,7 @@ mod profile; | ... | @@ -4,7 +4,7 @@ mod profile; |
| 4 | 4 | ||
| 5 | use crate::{ | 5 | use crate::{ |
| 6 | layout::{TextBrush, TextLayout}, | 6 | layout::{TextBrush, TextLayout}, |
| 7 | outline::{BoxMark, TagIcon}, | 7 | outline::TagIcon, |
| 8 | }; | 8 | }; |
| 9 | use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError}; | 9 | use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError}; |
| 10 | use parley::PositionedLayoutItem; | 10 | use parley::PositionedLayoutItem; |
| ... | @@ -140,116 +140,199 @@ pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout { | ... | @@ -140,116 +140,199 @@ pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout { |
| 140 | .expect("Page text is a TextLayout") | 140 | .expect("Page text is a TextLayout") |
| 141 | } | 141 | } |
| 142 | 142 | ||
| 143 | const CHECKBOX: &str = include_str!("../../assets/tags/checkbox.svg"); | ||
| 144 | const CHECKMARK: &str = include_str!("../../assets/tags/checkmark.svg"); | 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 | const FLAG: &str = include_str!("../../assets/tags/flag.svg"); | 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 | const BOX_SMALL: &str = include_str!("../../assets/tags/box-small.svg"); | 145 | const BOX_SMALL: &str = include_str!("../../assets/tags/box-small.svg"); |
| 165 | const CHECK_SMALL: &str = include_str!("../../assets/tags/check-small.svg"); | 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 | const TAG: &str = include_str!("../../assets/tags/tag.svg"); | 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 | /// The tag's artwork, drawn in order. | 318 | /// The tag's artwork, drawn in order. |
| 175 | pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] { | 319 | pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] { |
| 176 | match icon { | 320 | static SOURCES: std::sync::OnceLock<Vec<[Vec<&'static str>; 2]>> = std::sync::OnceLock::new(); |
| 177 | TagIcon::CheckBox { | 321 | let TagIcon::Symbol { shape, checked } = icon else { |
| 178 | checked: false, | 322 | return &[FLAG]; |
| 179 | mark: None, | 323 | }; |
| 180 | } => &[CHECKBOX], | 324 | let sources = SOURCES.get_or_init(|| { |
| 181 | TagIcon::CheckBox { | 325 | (0..=SYMBOLS) |
| 182 | checked: true, | 326 | .map(|shape| match art(shape) { |
| 183 | mark: None, | 327 | Art::Box(art) => [vec![art], vec![art, CHECKMARK]], |
| 184 | } => &[CHECKBOX, CHECKMARK], | 328 | Art::Badged(badge) => [vec![BOX_SMALL, badge], vec![BOX_SMALL, CHECK_SMALL, badge]], |
| 185 | TagIcon::CheckBox { | 329 | Art::Plain(art) => [vec![art], vec![art]], |
| 186 | checked: false, | 330 | }) |
| 187 | mark: Some(BoxMark::Person), | 331 | .collect() |
| 188 | } => &[BOX_SMALL, MARK_PERSON], | 332 | }); |
| 189 | TagIcon::CheckBox { | 333 | match sources.get(usize::from(shape)) { |
| 190 | checked: true, | 334 | Some(sources) => &sources[usize::from(checked)], |
| 191 | mark: Some(BoxMark::Person), | 335 | None => &[TAG], |
| 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], | ||
| 253 | } | 336 | } |
| 254 | } | 337 | } |
| 255 | 338 | ||
| ... | @@ -364,29 +447,26 @@ mod tests { | ... | @@ -364,29 +447,26 @@ mod tests { |
| 364 | mapped_at_creation: false, | 447 | mapped_at_creation: false, |
| 365 | }); | 448 | }); |
| 366 | let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); | 449 | let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); |
| 367 | let icons = [ | 450 | let icons: Vec<_> = [ |
| 368 | TagIcon::CheckBox { | 451 | (3, false), |
| 369 | checked: false, | 452 | (3, true), |
| 370 | mark: None, | 453 | (13, false), |
| 371 | }, | 454 | (15, false), |
| 372 | TagIcon::CheckBox { | 455 | (136, false), |
| 373 | checked: true, | 456 | (118, false), |
| 374 | mark: None, | 457 | (23, false), |
| 375 | }, | 458 | (18, false), |
| 376 | TagIcon::Star, | 459 | (121, false), |
| 377 | TagIcon::Question, | 460 | (17, false), |
| 378 | TagIcon::Highlight, | 461 | (100, false), |
| 379 | TagIcon::Contact, | 462 | (101, false), |
| 380 | TagIcon::Address, | 463 | (102, false), |
| 381 | TagIcon::Phone, | 464 | (999, false), |
| 382 | TagIcon::Music, | 465 | ] |
| 383 | TagIcon::Exclamation, | 466 | .into_iter() |
| 384 | TagIcon::RedSquare, | 467 | .map(|(shape, checked)| TagIcon::of(shape, checked).unwrap()) |
| 385 | TagIcon::YellowSquare, | 468 | .chain([TagIcon::Task]) |
| 386 | TagIcon::BlueSquare, | 469 | .collect(); |
| 387 | TagIcon::Flag, | ||
| 388 | TagIcon::Other, | ||
| 389 | ]; | ||
| 390 | let text = TextEngine::default() | 470 | let text = TextEngine::default() |
| 391 | .layout( | 471 | .layout( |
| 392 | &Paragraph::from_runs([ | 472 | &Paragraph::from_runs([ |
crates/canvas/src/gpu/page.rs+139-39| ... | @@ -79,6 +79,7 @@ pub enum SceneHit<T> { | ... | @@ -79,6 +79,7 @@ pub enum SceneHit<T> { |
| 79 | Date(DateField), | 79 | Date(DateField), |
| 80 | ReadOnly(usize), | 80 | ReadOnly(usize), |
| 81 | Image(onestore::ExGuid), | 81 | Image(onestore::ExGuid), |
| 82 | File(onestore::ExGuid), | ||
| 82 | } | 83 | } |
| 83 | 84 | ||
| 84 | fn outline_origin( | 85 | fn outline_origin( |
| ... | @@ -139,6 +140,14 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec< | ... | @@ -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 | fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) { | 151 | fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) { |
| 143 | primitives.push(Primitive::Rect { | 152 | primitives.push(Primitive::Rect { |
| 144 | rect, | 153 | rect, |
| ... | @@ -230,10 +239,9 @@ impl PageScene { | ... | @@ -230,10 +239,9 @@ impl PageScene { |
| 230 | } | 239 | } |
| 231 | } | 240 | } |
| 232 | for (id, art) in &mut scene.backgrounds { | 241 | for (id, art) in &mut scene.backgrounds { |
| 233 | if let Some(shown) = self.backgrounds.remove(id) | 242 | match self.backgrounds.remove(id) { |
| 234 | && shown.same(art) | 243 | Some(shown) if shown.same(art) => *art = shown, |
| 235 | { | 244 | _ => art.inline = true, |
| 236 | *art = shown; | ||
| 237 | } | 245 | } |
| 238 | } | 246 | } |
| 239 | scene.mark_unavailable(&mut editor.objects, engine)?; | 247 | scene.mark_unavailable(&mut editor.objects, engine)?; |
| ... | @@ -241,6 +249,28 @@ impl PageScene { | ... | @@ -241,6 +249,28 @@ impl PageScene { |
| 241 | Ok(()) | 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 | /// Turns page pictures the renderer cannot decode into placeholders; their stored data | 274 | /// Turns page pictures the renderer cannot decode into placeholders; their stored data |
| 245 | /// is kept. | 275 | /// is kept. |
| 246 | fn mark_unavailable( | 276 | fn mark_unavailable( |
| ... | @@ -275,7 +305,6 @@ impl PageScene { | ... | @@ -275,7 +305,6 @@ impl PageScene { |
| 275 | onestore::page::ParagraphContent::Image(image) => { | 305 | onestore::page::ParagraphContent::Image(image) => { |
| 276 | payloads.push((image.id, image.bytes.as_ref())) | 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 | onestore::page::ParagraphContent::Attachment(file) => { | 308 | onestore::page::ParagraphContent::Attachment(file) => { |
| 280 | if let Some(icon) = file.preview.as_ref() { | 309 | if let Some(icon) = file.preview.as_ref() { |
| 281 | payloads.push((file.id, Some(icon))) | 310 | payloads.push((file.id, Some(icon))) |
| ... | @@ -303,6 +332,11 @@ impl PageScene { | ... | @@ -303,6 +332,11 @@ impl PageScene { |
| 303 | payloads.push((source.id, source.bytes.as_ref())) | 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 | Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads), | 340 | Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads), |
| 307 | Content::Editable(id) => { | 341 | Content::Editable(id) => { |
| 308 | if let Some(outline) = | 342 | if let Some(outline) = |
| ... | @@ -396,6 +430,11 @@ impl PageScene { | ... | @@ -396,6 +430,11 @@ impl PageScene { |
| 396 | rects.push((source.id, [x, y, x + width, y + height])); | 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 | Content::Outline { | 438 | Content::Outline { |
| 400 | source, | 439 | source, |
| 401 | layout, | 440 | layout, |
| ... | @@ -433,40 +472,61 @@ impl PageScene { | ... | @@ -433,40 +472,61 @@ impl PageScene { |
| 433 | primitives: &mut Vec<Primitive<'a>>, | 472 | primitives: &mut Vec<Primitive<'a>>, |
| 434 | ) { | 473 | ) { |
| 435 | for object in &outline.objects { | 474 | for object in &outline.objects { |
| 436 | let [x0, y0, x1, y1] = object.rect; | 475 | self.append_object(object, origin, moving, paper, primitives); |
| 437 | let rect = match moving { | 476 | } |
| 438 | Some((id, rect)) if id == object.id => rect, | 477 | } |
| 439 | _ => [ | 478 | |
| 440 | x0 + origin[0], | 479 | /// One picture, file, drawing or placeholder laid out from `origin`; `moving` as for |
| 441 | y0 + origin[1], | 480 | /// `append_outline_objects`. |
| 442 | x1 + origin[0], | 481 | fn append_object<'a>( |
| 443 | y1 + origin[1], | 482 | &'a self, |
| 444 | ], | 483 | object: &'a crate::outline::ObjectLayout, |
| 445 | }; | 484 | origin: [f32; 2], |
| 446 | let picture = self.pictures.get(&object.id); | 485 | moving: Option<(onestore::ExGuid, [f32; 4])>, |
| 447 | if let Some(image) = picture.and_then(Picture::image) { | 486 | paper: Paper, |
| 448 | primitives.push(Primitive::Image { image, rect }); | 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 { | 508 | crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => { |
| 451 | crate::outline::ObjectKind::Ink(ink) => { | 509 | append_placeholder(rect, paper, primitives) |
| 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(_) => {} | ||
| 461 | } | 510 | } |
| 462 | if let Some(label) = object.label() { | 511 | // A file stored without the icon OneNote rendered for it shows a blank page. |
| 463 | primitives.push(Primitive::Text { | 512 | crate::outline::ObjectKind::File(_) if picture.is_none() => { |
| 464 | text: &label.text, | 513 | primitives.push(Primitive::Icon { |
| 465 | origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]], | 514 | sources: &[FILE_ICON], |
| 466 | clip: None, | 515 | origin: [rect[0], rect[1]], |
| 467 | ink: paper.ink, | 516 | size: rect[2] - rect[0], |
| 468 | }); | 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,6 +618,7 @@ impl PageScene { |
| 558 | ], | 618 | ], |
| 559 | false, | 619 | false, |
| 560 | )), | 620 | )), |
| 621 | Content::File { .. } => Some((object.file()?.1, true)), | ||
| 561 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), | 622 | Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)), |
| 562 | Content::ReadOnly(object) => Some((object.rect(), true)), | 623 | Content::ReadOnly(object) => Some((object.rect(), true)), |
| 563 | Content::Editable(_) => None, | 624 | Content::Editable(_) => None, |
| ... | @@ -652,6 +713,12 @@ impl PageScene { | ... | @@ -652,6 +713,12 @@ impl PageScene { |
| 652 | return Some(SceneHit::Image(source.id)); | 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 | Content::Outline { .. } | Content::Image(_) | Content::Ink(_) => {} | 722 | Content::Outline { .. } | Content::Image(_) | Content::Ink(_) => {} |
| 656 | } | 723 | } |
| 657 | } | 724 | } |
| ... | @@ -737,13 +804,43 @@ impl PageScene { | ... | @@ -737,13 +804,43 @@ impl PageScene { |
| 737 | append_ink(ink, [offset[0] + x, offset[1] + y], primitives); | 804 | append_ink(ink, [offset[0] + x, offset[1] + y], primitives); |
| 738 | continue; | 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 | let object_origin = [origin[0] + offset[0], origin[1] + offset[1]]; | 823 | let object_origin = [origin[0] + offset[0], origin[1] + offset[1]]; |
| 742 | match content { | 824 | match content { |
| 743 | Content::Image(source) => { | 825 | Content::Image(source) => { |
| 744 | // The paper shows until the worker's raster lands. | ||
| 745 | let (image, [width, height]) = match self.backgrounds.get(&source.id) { | 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 | None => { | 844 | None => { |
| 748 | let picture = self | 845 | let picture = self |
| 749 | .pictures | 846 | .pictures |
| ... | @@ -816,7 +913,10 @@ impl PageScene { | ... | @@ -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 | Ok(()) | 922 | Ok(()) |
crates/canvas/src/gpu/page/background.rs+54-17| ... | @@ -402,6 +402,10 @@ const MARKER: &str = "Snowbound template"; | ... | @@ -402,6 +402,10 @@ const MARKER: &str = "Snowbound template"; |
| 402 | /// device pixels per point. | 402 | /// device pixels per point. |
| 403 | const MAX_RASTER_BYTES: f32 = 16.0 * 1024.0 * 1024.0; | 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 | /// Template art standing in for a page's background picture. | 409 | /// Template art standing in for a page's background picture. |
| 406 | pub(super) struct Background { | 410 | pub(super) struct Background { |
| 407 | svg: &'static str, | 411 | svg: &'static str, |
| ... | @@ -409,14 +413,19 @@ pub(super) struct Background { | ... | @@ -409,14 +413,19 @@ pub(super) struct Background { |
| 409 | pub size: [f32; 2], | 413 | pub size: [f32; 2], |
| 410 | /// For light and for dark paper. | 414 | /// For light and for dark paper. |
| 411 | variants: [Variant; 2], | 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 | #[derive(Default)] | 423 | #[derive(Default)] |
| 415 | struct Variant { | 424 | struct Variant { |
| 416 | /// The raster painted and its device pixels per point. | 425 | /// The raster painted and what it was made for. |
| 417 | shown: Option<(RasterImage, f32)>, | 426 | shown: Option<(RasterImage, Made)>, |
| 418 | /// The one raster being made and its device pixels per point; replacing it abandons it. | 427 | /// The one raster being made and what for; replacing it abandons it. |
| 419 | pending: Option<(Slot<RasterImage>, f32)>, | 428 | pending: Option<(Slot<RasterImage>, Made)>, |
| 420 | } | 429 | } |
| 421 | 430 | ||
| 422 | impl Background { | 431 | impl Background { |
| ... | @@ -451,6 +460,7 @@ impl Background { | ... | @@ -451,6 +460,7 @@ impl Background { |
| 451 | svg, | 460 | svg, |
| 452 | size, | 461 | size, |
| 453 | variants: Default::default(), | 462 | variants: Default::default(), |
| 463 | inline: false, | ||
| 454 | }) | 464 | }) |
| 455 | } | 465 | } |
| 456 | 466 | ||
| ... | @@ -469,38 +479,44 @@ impl Background { | ... | @@ -469,38 +479,44 @@ impl Background { |
| 469 | let density = density(scale) | 479 | let density = density(scale) |
| 470 | .min((MAX_RASTER_BYTES / (4.0 * width * height)).sqrt()) | 480 | .min((MAX_RASTER_BYTES / (4.0 * width * height)).sqrt()) |
| 471 | .min(4096.0 / width.max(height)); | 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 | let variant = &mut self.variants[usize::from(dark)]; | 489 | let variant = &mut self.variants[usize::from(dark)]; |
| 473 | let landed = variant | 490 | let landed = variant |
| 474 | .pending | 491 | .pending |
| 475 | .as_ref() | 492 | .as_ref() |
| 476 | .and_then(|(slot, made)| Some((slot.lock().unwrap().take()?, *made))); | 493 | .and_then(|(slot, made)| Some((slot.lock().unwrap().take()?, *made))); |
| 477 | if let Some((image, made)) = landed { | 494 | if let Some((image, landed)) = landed { |
| 478 | if made == density || variant.shown.is_none() { | 495 | if landed == made || variant.shown.is_none() { |
| 479 | variant.shown = Some((image, made)); | 496 | variant.shown = Some((image, landed)); |
| 480 | } | 497 | } |
| 481 | variant.pending = None; | 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 | let shown = variant.shown.as_ref().map(|(_, made)| *made); | 504 | let shown = variant.shown.as_ref().map(|(_, made)| *made); |
| 484 | if shown != Some(density) | 505 | if shown != Some(made) && variant.pending.as_ref().map(|(_, made)| *made) != Some(made) { |
| 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); | ||
| 490 | let start = Instant::now() | 506 | let start = Instant::now() |
| 491 | + if shown.is_some() { | 507 | + if shown.is_some() { |
| 492 | SETTLE | 508 | SETTLE |
| 493 | } else { | 509 | } else { |
| 494 | Duration::ZERO | 510 | Duration::ZERO |
| 495 | }; | 511 | }; |
| 496 | let slot = queue(start, waker, move || rasterize(svg, paper, size)); | 512 | let slot = queue(start, waker, raster(density)); |
| 497 | variant.pending = Some((slot, 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 | let [lightness, ..] = draw::oklab(paper.color); | 520 | let [lightness, ..] = draw::oklab(paper.color); |
| 505 | let [ink, ..] = draw::oklab(paper.ink); | 521 | let [ink, ..] = draw::oklab(paper.ink); |
| 506 | ink > lightness | 522 | ink > lightness |
| ... | @@ -514,6 +530,19 @@ fn rasterize(svg: &str, dark: Option<Paper>, size: [u32; 2]) -> RasterImage { | ... | @@ -514,6 +530,19 @@ fn rasterize(svg: &str, dark: Option<Paper>, size: [u32; 2]) -> RasterImage { |
| 514 | RasterImage::new(size, pixels).expect("Art is within the image budget") | 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 | /// Straight-alpha RGBA rows of `svg` stretched to `width` by `height` pixels. | 546 | /// Straight-alpha RGBA rows of `svg` stretched to `width` by `height` pixels. |
| 518 | fn render(svg: &str, [width, height]: [u32; 2]) -> Vec<u8> { | 547 | fn render(svg: &str, [width, height]: [u32; 2]) -> Vec<u8> { |
| 519 | let tree = parse(svg); | 548 | let tree = parse(svg); |
| ... | @@ -747,6 +776,7 @@ mod tests { | ... | @@ -747,6 +776,7 @@ mod tests { |
| 747 | svg: TEMPLATES.iter().find(|t| t.name == "ivy").unwrap().svg, | 776 | svg: TEMPLATES.iter().find(|t| t.name == "ivy").unwrap().svg, |
| 748 | size: [64.0, 48.0], | 777 | size: [64.0, 48.0], |
| 749 | variants: Default::default(), | 778 | variants: Default::default(), |
| 779 | inline: false, | ||
| 750 | } | 780 | } |
| 751 | } | 781 | } |
| 752 | 782 | ||
| ... | @@ -805,6 +835,13 @@ mod tests { | ... | @@ -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 | #[test] | 845 | #[test] |
| 809 | fn our_template_pictures_are_recognised_as_the_template() { | 846 | fn our_template_pictures_are_recognised_as_the_template() { |
| 810 | for (name, magic, size) in [ | 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,38 +594,15 @@ fn runs_of(shaped: &ParagraphLayout, breaks: bool) -> Result<Vec<(Node, Vec<u32> |
| 594 | .tags | 594 | .tags |
| 595 | .iter() | 595 | .iter() |
| 596 | .map(|tag| { | 596 | .map(|tag| { |
| 597 | use crate::outline::TagIcon; | 597 | use crate::outline::{TagIcon, symbol_name}; |
| 598 | let fallback = match tag.icon { | 598 | let label = match tag.icon { |
| 599 | TagIcon::CheckBox { .. } => "To do", | 599 | _ if !tag.label.is_empty() => &tag.label, |
| 600 | TagIcon::Star => "Important", | 600 | TagIcon::Symbol { shape, .. } => symbol_name(shape).unwrap_or("Tag"), |
| 601 | TagIcon::Question => "Question", | 601 | TagIcon::Task => "Outlook task", |
| 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 | ||
| 625 | }; | 602 | }; |
| 626 | let state = match tag.icon { | 603 | let state = match tag.icon { |
| 627 | TagIcon::CheckBox { checked: true, .. } => ", completed", | 604 | TagIcon::Symbol { checked: true, .. } => ", completed", |
| 628 | TagIcon::CheckBox { checked: false, .. } => ", incomplete", | 605 | icon if icon.checkable() => ", incomplete", |
| 629 | _ => "", | 606 | _ => "", |
| 630 | }; | 607 | }; |
| 631 | format!( | 608 | format!( |
| ... | @@ -1026,7 +1003,7 @@ pub(super) mod tests { | ... | @@ -1026,7 +1003,7 @@ pub(super) mod tests { |
| 1026 | "Rehearsal, incomplete", | 1003 | "Rehearsal, incomplete", |
| 1027 | "Rehearsal, completed", | 1004 | "Rehearsal, completed", |
| 1028 | "Question", | 1005 | "Question", |
| 1029 | "Music, disabled" | 1006 | "Music Note, disabled" |
| 1030 | ] | 1007 | ] |
| 1031 | ); | 1008 | ); |
| 1032 | let text = state.focus().unwrap().document_range().text(); | 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,6 +60,8 @@ pub enum Request { |
| 60 | Paste, | 60 | Paste, |
| 61 | /// Open a link's address, as a click on a link does. | 61 | /// Open a link's address, as a click on a link does. |
| 62 | OpenLink(String), | 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 | /// What a secondary press lands on, which its context menu offers commands for. | 67 | /// What a secondary press lands on, which its context menu offers commands for. |
| ... | @@ -72,6 +74,8 @@ pub struct Context { | ... | @@ -72,6 +74,8 @@ pub struct Context { |
| 72 | pub selected: bool, | 74 | pub selected: bool, |
| 73 | /// The paragraph at the caret, which Copy Link to Paragraph names. | 75 | /// The paragraph at the caret, which Copy Link to Paragraph names. |
| 74 | pub paragraph: Option<onestore::ExGuid>, | 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 | /// What an event did: `changed` means the page or selection changed (the host saves, | 81 | /// What an event did: `changed` means the page or selection changed (the host saves, |
| ... | @@ -175,13 +179,15 @@ enum PointerFeedback<'a> { | ... | @@ -175,13 +179,15 @@ enum PointerFeedback<'a> { |
| 175 | pub enum ObjectFocus { | 179 | pub enum ObjectFocus { |
| 176 | ReadOnly(usize), | 180 | ReadOnly(usize), |
| 177 | Image(onestore::ExGuid), | 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 | impl ObjectFocus { | 186 | impl ObjectFocus { |
| 181 | pub fn read_only(self) -> Option<usize> { | 187 | pub fn read_only(self) -> Option<usize> { |
| 182 | match self { | 188 | match self { |
| 183 | Self::ReadOnly(index) => Some(index), | 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,6 +525,7 @@ impl PageView { |
| 519 | let (origin, size) = self.editor.image_placement(id).unwrap(); | 525 | let (origin, size) = self.editor.image_placement(id).unwrap(); |
| 520 | image_rect(origin, size) | 526 | image_rect(origin, size) |
| 521 | } | 527 | } |
| 528 | ObjectFocus::File(id) => self.editor.attachment_rect(id).unwrap(), | ||
| 522 | }; | 529 | }; |
| 523 | [ | 530 | [ |
| 524 | x0 + offset[0], | 531 | x0 + offset[0], |
| ... | @@ -861,7 +868,7 @@ impl PageView { | ... | @@ -861,7 +868,7 @@ impl PageView { |
| 861 | { | 868 | { |
| 862 | Cursor::Pointer | 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 | _ => Cursor::Text, | 872 | _ => Cursor::Text, |
| 866 | } | 873 | } |
| 867 | } | 874 | } |
| ... | @@ -998,6 +1005,24 @@ impl PageView { | ... | @@ -998,6 +1005,24 @@ impl PageView { |
| 998 | .click_check(&mut self.engine, outline, paragraph)?; | 1005 | .click_check(&mut self.engine, outline, paragraph)?; |
| 999 | return self.changed(); | 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 | Some(Hit::Image { id, handle }) => { | 1026 | Some(Hit::Image { id, handle }) => { |
| 1002 | self.set_object_focus(Some(ObjectFocus::Image(id))); | 1027 | self.set_object_focus(Some(ObjectFocus::Image(id))); |
| 1003 | self.drag = Some(Drag::Image { | 1028 | self.drag = Some(Drag::Image { |
| ... | @@ -1062,15 +1087,7 @@ impl PageView { | ... | @@ -1062,15 +1087,7 @@ impl PageView { |
| 1062 | }); | 1087 | }); |
| 1063 | } | 1088 | } |
| 1064 | None => { | 1089 | None => { |
| 1065 | // A fresh click places text 7 px above the pointer, on the grid. | 1090 | self.place_caret(point)?; |
| 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)?; | ||
| 1074 | self.set_object_focus(None); | 1091 | self.set_object_focus(None); |
| 1075 | self.drag = Some(Drag::Text { | 1092 | self.drag = Some(Drag::Text { |
| 1076 | anchor: self.editor.selection(), | 1093 | anchor: self.editor.selection(), |
| ... | @@ -1081,6 +1098,19 @@ impl PageView { | ... | @@ -1081,6 +1098,19 @@ impl PageView { |
| 1081 | self.changed() | 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 | pub fn pointer_released(&mut self) -> Result<Response> { | 1114 | pub fn pointer_released(&mut self) -> Result<Response> { |
| 1085 | if let Some(Drag::Space { axis, line }) = self.drag { | 1115 | if let Some(Drag::Space { axis, line }) = self.drag { |
| 1086 | self.drag = None; | 1116 | self.drag = None; |
| ... | @@ -1261,11 +1291,22 @@ impl PageView { | ... | @@ -1261,11 +1291,22 @@ impl PageView { |
| 1261 | } | 1291 | } |
| 1262 | 1292 | ||
| 1263 | /// A secondary press at the pointer: text there takes the caret unless it lies in the | 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 | pub fn context(&mut self) -> Result<Option<(Response, Context)>> { | 1296 | pub fn context(&mut self) -> Result<Option<(Response, Context)>> { |
| 1266 | let point = self.viewport.document_point(self.pointer); | 1297 | let point = self.viewport.document_point(self.pointer); |
| 1267 | let Some(Hit::Text { id, point }) = self.hit_test(point) else { | 1298 | let (id, point) = match self.hit_test(point) { |
| 1268 | return Ok(None); | 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 | if self.object_focus.is_some() || !self.accepts_text() { | 1311 | if self.object_focus.is_some() || !self.accepts_text() { |
| 1271 | return Ok(None); | 1312 | return Ok(None); |
| ... | @@ -1296,6 +1337,7 @@ impl PageView { | ... | @@ -1296,6 +1337,7 @@ impl PageView { |
| 1296 | .document() | 1337 | .document() |
| 1297 | .leaf(anchor.min(focus).paragraph) | 1338 | .leaf(anchor.min(focus).paragraph) |
| 1298 | .map(|(_, _, node)| node.id), | 1339 | .map(|(_, _, node)| node.id), |
| 1340 | attachment: None, | ||
| 1299 | }; | 1341 | }; |
| 1300 | Ok(Some((self.changed()?, context))) | 1342 | Ok(Some((self.changed()?, context))) |
| 1301 | } | 1343 | } |
| ... | @@ -1363,6 +1405,44 @@ impl PageView { | ... | @@ -1363,6 +1405,44 @@ impl PageView { |
| 1363 | self.edited() | 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 | /// Professional, or Linear with `linear`, on the equation at the caret. | 1446 | /// Professional, or Linear with `linear`, on the equation at the caret. |
| 1367 | pub fn switch_equation(&mut self, linear: bool) -> Result<Response> { | 1447 | pub fn switch_equation(&mut self, linear: bool) -> Result<Response> { |
| 1368 | if linear { | 1448 | if linear { |
| ... | @@ -1380,7 +1460,8 @@ impl PageView { | ... | @@ -1380,7 +1460,8 @@ impl PageView { |
| 1380 | } else { | 1460 | } else { |
| 1381 | self.editor.undo(&mut self.engine)?; | 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 | && self.editor.image_placement(id).is_none() | 1465 | && self.editor.image_placement(id).is_none() |
| 1385 | { | 1466 | { |
| 1386 | self.set_object_focus(None); | 1467 | self.set_object_focus(None); |
| ... | @@ -1404,18 +1485,29 @@ impl PageView { | ... | @@ -1404,18 +1485,29 @@ impl PageView { |
| 1404 | self.changed() | 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 | pub fn set_paper( | 1490 | pub fn set_paper( |
| 1409 | &mut self, | 1491 | &mut self, |
| 1410 | color: Option<u32>, | 1492 | color: Option<u32>, |
| 1411 | rule_lines: Option<onestore::page::RuleLines>, | 1493 | rule_lines: Option<onestore::page::RuleLines>, |
| 1494 | art: Option<Vec<onestore::page::Image>>, | ||
| 1412 | ) -> Result<Response> { | 1495 | ) -> Result<Response> { |
| 1413 | if !self.editor.set_paper(color, rule_lines) { | 1496 | if !self.editor.set_paper(color, rule_lines, art) { |
| 1414 | return Ok(Response::default()); | 1497 | return Ok(Response::default()); |
| 1415 | } | 1498 | } |
| 1499 | self.follow_pictures()?; | ||
| 1416 | self.changed() | 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 | /// Gives keyboard focus to a read-only object, as an assistive technology asks. | 1511 | /// Gives keyboard focus to a read-only object, as an assistive technology asks. |
| 1420 | pub fn focus_read_only(&mut self, index: usize) -> Result<Response> { | 1512 | pub fn focus_read_only(&mut self, index: usize) -> Result<Response> { |
| 1421 | self.set_object_focus(Some(ObjectFocus::ReadOnly(index))); | 1513 | self.set_object_focus(Some(ObjectFocus::ReadOnly(index))); |
| ... | @@ -1439,14 +1531,14 @@ impl PageView { | ... | @@ -1439,14 +1531,14 @@ impl PageView { |
| 1439 | if key == &Key::Named(NamedKey::Modifier) { | 1531 | if key == &Key::Named(NamedKey::Modifier) { |
| 1440 | return Ok(Response::default()); | 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 | && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete)) | 1535 | && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete)) |
| 1444 | { | 1536 | { |
| 1445 | self.editor.remove_image(&mut self.engine, id)?; | 1537 | self.editor.remove_image(&mut self.engine, id)?; |
| 1446 | self.set_object_focus(None); | 1538 | self.set_object_focus(None); |
| 1447 | return self.changed(); | 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 | && !(shift || command || option || control) | 1542 | && !(shift || command || option || control) |
| 1451 | && let Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) = key | 1543 | && let Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) = key |
| 1452 | && self.editor.step_from_image( | 1544 | && self.editor.step_from_image( |
| ... | @@ -1671,6 +1763,8 @@ pub enum Hit { | ... | @@ -1671,6 +1763,8 @@ pub enum Hit { |
| 1671 | id: onestore::ExGuid, | 1763 | id: onestore::ExGuid, |
| 1672 | handle: [i8; 2], | 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 | /// What a document point lands on; `pixel` is document points per device pixel. | 1770 | /// What a document point lands on; `pixel` is document points per device pixel. |
| ... | @@ -1762,7 +1856,7 @@ fn page_hit( | ... | @@ -1762,7 +1856,7 @@ fn page_hit( |
| 1762 | paragraph.tags.iter().any(|tag| { | 1856 | paragraph.tags.iter().any(|tag| { |
| 1763 | let tag_x = left + shaped.tag_column_offset() + tag.origin[0]; | 1857 | let tag_x = left + shaped.tag_column_offset() + tag.origin[0]; |
| 1764 | let tag_y = top + paragraph.origin[1] + tag.origin[1]; | 1858 | let tag_y = top + paragraph.origin[1] + tag.origin[1]; |
| 1765 | matches!(tag.icon, crate::outline::TagIcon::CheckBox { .. }) | 1859 | tag.icon.checkable() |
| 1766 | && (tag_x..=tag_x + tag.size).contains(&x) | 1860 | && (tag_x..=tag_x + tag.size).contains(&x) |
| 1767 | && (tag_y..=tag_y + tag.size).contains(&y) | 1861 | && (tag_y..=tag_y + tag.size).contains(&y) |
| 1768 | }) | 1862 | }) |
| ... | @@ -1791,6 +1885,14 @@ fn page_hit( | ... | @@ -1791,6 +1885,14 @@ fn page_hit( |
| 1791 | handle: [0, 0], | 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 | if x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3] { | 1896 | if x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3] { |
| 1795 | return Some(Hit::Text { | 1897 | return Some(Hit::Text { |
| 1796 | id: outline.id, | 1898 | id: outline.id, |
| ... | @@ -1826,6 +1928,7 @@ fn page_hit( | ... | @@ -1826,6 +1928,7 @@ fn page_hit( |
| 1826 | crate::gpu::page::SceneHit::Date(field) => Some(Hit::Date(field)), | 1928 | crate::gpu::page::SceneHit::Date(field) => Some(Hit::Date(field)), |
| 1827 | crate::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), | 1929 | crate::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), |
| 1828 | crate::gpu::page::SceneHit::Image(id) => Some(Hit::Image { id, handle: [0, 0] }), | 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 | [Layer::Grips, Layer::Body, Layer::Below] | 1934 | [Layer::Grips, Layer::Body, Layer::Below] |
| ... | @@ -2010,6 +2113,29 @@ fn page_primitives<'a>( | ... | @@ -2010,6 +2113,29 @@ fn page_primitives<'a>( |
| 2010 | }; | 2113 | }; |
| 2011 | append_image_chrome(image_rect(origin, size), paint.pixel, &mut primitives); | 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 | if let Some(ObjectFocus::ReadOnly(index)) = object_focus { | 2139 | if let Some(ObjectFocus::ReadOnly(index)) = object_focus { |
| 2014 | let (scene, offset) = scene.unwrap(); | 2140 | let (scene, offset) = scene.unwrap(); |
| 2015 | let [x0, y0, x1, y1] = scene.read_only(Some(editor)).nth(index).unwrap().rect(); | 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,7 +1805,7 @@ fn outline_chrome_grows_left_to_contain_its_tag_column() { |
| 1805 | assert!(frame[1] < y && y + size < frame[3], "{frame:?} {y}"); | 1805 | assert!(frame[1] < y && y + size < frame[3], "{frame:?} {y}"); |
| 1806 | // OneNote 2010 shows an arrow over a check box, which a click toggles. | 1806 | // OneNote 2010 shows an arrow over a check box, which a click toggles. |
| 1807 | let hit = page_hit_test(editor, None, [x + 1.0, y + 1.0], pixel); | 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 | assert_eq!( | 1809 | assert_eq!( |
| 1810 | hit, | 1810 | hit, |
| 1811 | Some(Hit::Check { | 1811 | Some(Hit::Check { |
| ... | @@ -1821,18 +1821,30 @@ fn outline_chrome_grows_left_to_contain_its_tag_column() { | ... | @@ -1821,18 +1821,30 @@ fn outline_chrome_grows_left_to_contain_its_tag_column() { |
| 1821 | }; | 1821 | }; |
| 1822 | check(&editor, 0.0); | 1822 | check(&editor, 0.0); |
| 1823 | editor | 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 | .unwrap(); | 1828 | .unwrap(); |
| 1826 | check(&editor, 1.0); | 1829 | check(&editor, 1.0); |
| 1827 | editor | 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 | .unwrap(); | 1835 | .unwrap(); |
| 1830 | check(&editor, 2.0); | 1836 | check(&editor, 2.0); |
| 1831 | editor | 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 | .unwrap(); | 1842 | .unwrap(); |
| 1834 | editor | 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 | .unwrap(); | 1848 | .unwrap(); |
| 1837 | check(&editor, 0.0); | 1849 | check(&editor, 0.0); |
| 1838 | } | 1850 | } |
| ... | @@ -1851,7 +1863,10 @@ fn a_click_on_a_check_box_toggles_it_under_an_arrow() { | ... | @@ -1851,7 +1863,10 @@ fn a_click_on_a_check_box_toggles_it_under_an_arrow() { |
| 1851 | let mut editor = | 1863 | let mut editor = |
| 1852 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); | 1864 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); |
| 1853 | editor | 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 | .unwrap(); | 1870 | .unwrap(); |
| 1856 | let outline = editor.active_outline(); | 1871 | let outline = editor.active_outline(); |
| 1857 | let (_, paragraph) = outline.layouts().next().unwrap(); | 1872 | let (_, paragraph) = outline.layouts().next().unwrap(); |
| ... | @@ -1923,7 +1938,10 @@ fn a_page_opens_where_onenote_places_the_view() { | ... | @@ -1923,7 +1938,10 @@ fn a_page_opens_where_onenote_places_the_view() { |
| 1923 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); | 1938 | CanvasEditor::from_text_outlines(vec![outline], Default::default(), None).unwrap(); |
| 1924 | if tag { | 1939 | if tag { |
| 1925 | editor | 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 | .unwrap(); | 1945 | .unwrap(); |
| 1928 | } | 1946 | } |
| 1929 | let reached = reach(editor.active_outline()); | 1947 | let reached = reach(editor.active_outline()); |
| ... | @@ -2625,3 +2643,242 @@ fn rule_lines_start_at_the_margin_origin() { | ... | @@ -2625,3 +2643,242 @@ fn rule_lines_start_at_the_margin_origin() { |
| 2625 | [0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0, 96.0] | 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,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 | /// The rect's top and bottom, the object's bottom and its label's top once the object | 103 | /// The rect's top and bottom, the object's bottom and its label's top once the object |
| 91 | /// starts at `y`, given its own height, and the height it takes in the flow. | 104 | /// starts at `y`, given its own height, and the height it takes in the flow. |
| 92 | fn at(&self, y: f32, height: f32) -> ([f32; 4], f32) { | 105 | fn at(&self, y: f32, height: f32) -> ([f32; 4], f32) { |
| ... | @@ -209,93 +222,192 @@ pub struct ParagraphLayout { | ... | @@ -209,93 +222,192 @@ pub struct ParagraphLayout { |
| 209 | pub band: Option<u32>, | 222 | pub band: Option<u32>, |
| 210 | } | 223 | } |
| 211 | 224 | ||
| 225 | /// How the page draws a note tag. | ||
| 212 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] | 226 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] |
| 213 | pub enum TagIcon { | 227 | pub enum TagIcon { |
| 214 | CheckBox { | 228 | /// Symbol `shape` of MS-ONE's NoteTagShape, `checked` only where it is a check box. |
| 215 | checked: bool, | 229 | Symbol { shape: u16, checked: bool }, |
| 216 | /// The badge on the small box of OneNote's check box variants. | 230 | /// An Outlook task, which OneNote 2010 flags. |
| 217 | mark: Option<BoxMark>, | 231 | Task, |
| 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, | ||
| 252 | } | 232 | } |
| 253 | 233 | ||
| 254 | impl TagIcon { | 234 | impl TagIcon { |
| 255 | /// How the page draws tag symbol `shape`, `checked` where it is a check box; none for | 235 | /// How the page draws tag symbol `shape`, `checked` where it is a check box; none for |
| 256 | /// shape 0, which marks the text instead. | 236 | /// shape 0, which marks the text instead. |
| 257 | pub fn of(shape: u16, checked: bool) -> Option<Self> { | 237 | pub fn of(shape: u16, checked: bool) -> Option<Self> { |
| 258 | Some(match shape { | 238 | (shape != 0).then_some(Self::Symbol { |
| 259 | 0 => return None, | 239 | shape, |
| 260 | // OneNote's check boxes, plain and with a person, arrow, number or bulb. | 240 | checked: checked && checkable(shape), |
| 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, | ||
| 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 | #[derive(Clone, Debug, PartialEq)] | 411 | #[derive(Clone, Debug, PartialEq)] |
| 300 | pub struct ParagraphTag { | 412 | pub struct ParagraphTag { |
| 301 | pub icon: TagIcon, | 413 | pub icon: TagIcon, |
| ... | @@ -367,6 +479,72 @@ fn caption(id: ExGuid, text_id: ExGuid, text: &str, format: Format) -> PageParag | ... | @@ -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 | /// A picture's displayed size: the user-set layout size, else its intrinsic size. | 548 | /// A picture's displayed size: the user-set layout size, else its intrinsic size. |
| 371 | pub(crate) fn image_size(image: &onestore::page::Image) -> Option<[f32; 2]> { | 549 | pub(crate) fn image_size(image: &onestore::page::Image) -> Option<[f32; 2]> { |
| 372 | let size = [ | 550 | let size = [ |
| ... | @@ -887,7 +1065,7 @@ impl ParagraphLayout { | ... | @@ -887,7 +1065,7 @@ impl ParagraphLayout { |
| 887 | for tag in &tag_definitions { | 1065 | for tag in &tag_definitions { |
| 888 | let (tag, shape, label) = (tag.0, tag.1, tag.2); | 1066 | let (tag, shape, label) = (tag.0, tag.1, tag.2); |
| 889 | let icon = match shape { | 1067 | let icon = match shape { |
| 890 | None => TagIcon::Flag, | 1068 | None => TagIcon::Task, |
| 891 | Some(shape) => match TagIcon::of(shape.unwrap_or(0), tag.status & 1 != 0) { | 1069 | Some(shape) => match TagIcon::of(shape.unwrap_or(0), tag.status & 1 != 0) { |
| 892 | Some(icon) => icon, | 1070 | Some(icon) => icon, |
| 893 | None => continue, | 1071 | None => continue, |
| ... | @@ -1149,30 +1327,8 @@ impl OutlineLayout { | ... | @@ -1149,30 +1327,8 @@ impl OutlineLayout { |
| 1149 | (object, h, x + w) | 1327 | (object, h, x + w) |
| 1150 | } | 1328 | } |
| 1151 | ParagraphContent::Attachment(file) => { | 1329 | ParagraphContent::Attachment(file) => { |
| 1152 | let [w, h] = file.size.unwrap_or([24.0, 24.0]); | 1330 | let (object, h) = |
| 1153 | let left = x + (ATTACHMENT_WIDTH - w) / 2.0; | 1331 | file_column(file, node.id, &node.format, x, shape)?; |
| 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 | }; | ||
| 1176 | (object, h, x + ATTACHMENT_WIDTH) | 1332 | (object, h, x + ATTACHMENT_WIDTH) |
| 1177 | } | 1333 | } |
| 1178 | ParagraphContent::Ink(ink) => { | 1334 | ParagraphContent::Ink(ink) => { |
| ... | @@ -2092,26 +2248,7 @@ mod tests { | ... | @@ -2092,26 +2248,7 @@ mod tests { |
| 2092 | task_id: None, | 2248 | task_id: None, |
| 2093 | extra_set: 0, | 2249 | extra_set: 0, |
| 2094 | }); | 2250 | }); |
| 2095 | for (shape, expected) in [ | 2251 | for shape in [3, 13, 15, 17, 18, 23, 118, 136, 100, 101, 102, 121] { |
| 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 | ] { | ||
| 2115 | definitions.insert( | 2252 | definitions.insert( |
| 2116 | id, | 2253 | id, |
| 2117 | Definition { | 2254 | Definition { |
| ... | @@ -2134,7 +2271,13 @@ mod tests { | ... | @@ -2134,7 +2271,13 @@ mod tests { |
| 2134 | &definitions, | 2271 | &definitions, |
| 2135 | ) | 2272 | ) |
| 2136 | .unwrap(); | 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 | let baseline = tagged.text.lines().next().unwrap().1.baseline; | 2281 | let baseline = tagged.text.lines().next().unwrap().1.baseline; |
| 2139 | assert_eq!( | 2282 | assert_eq!( |
| 2140 | tagged.tags[0].origin, | 2283 | tagged.tags[0].origin, |
| ... | @@ -2203,12 +2346,18 @@ mod tests { | ... | @@ -2203,12 +2346,18 @@ mod tests { |
| 2203 | ) | 2346 | ) |
| 2204 | .unwrap() | 2347 | .unwrap() |
| 2205 | }; | 2348 | }; |
| 2206 | // A symbol without artwork shows as a plain tag, an Outlook task as its flag, and a | 2349 | // A symbol MS-ONE does not list keeps its number, an Outlook task shows its flag, and |
| 2207 | // tag whose definition is missing as nothing. | 2350 | // a tag whose definition is missing shows nothing. |
| 2208 | assert_eq!(shaped(&node, &definitions).tags[0].icon, TagIcon::Other); | 2351 | assert_eq!( |
| 2352 | shaped(&node, &definitions).tags[0].icon, | ||
| 2353 | TagIcon::Symbol { | ||
| 2354 | shape: 999, | ||
| 2355 | checked: false | ||
| 2356 | } | ||
| 2357 | ); | ||
| 2209 | let mut task = node.clone(); | 2358 | let mut task = node.clone(); |
| 2210 | task.text_mut().unwrap().tags[0].status |= 4; | 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 | let mut untagged = definitions.clone(); | 2361 | let mut untagged = definitions.clone(); |
| 2213 | untagged.remove(&id); | 2362 | untagged.remove(&id); |
| 2214 | assert!(shaped(&node, &untagged).tags.is_empty()); | 2363 | assert!(shaped(&node, &untagged).tags.is_empty()); |
| ... | @@ -2431,6 +2580,7 @@ mod tests { | ... | @@ -2431,6 +2580,7 @@ mod tests { |
| 2431 | filename: "notes 🦀.txt".into(), | 2580 | filename: "notes 🦀.txt".into(), |
| 2432 | source_path: None, | 2581 | source_path: None, |
| 2433 | size: Some([24.0, 24.0]), | 2582 | size: Some([24.0, 24.0]), |
| 2583 | layout: Default::default(), | ||
| 2434 | bytes: None, | 2584 | bytes: None, |
| 2435 | preview: None, | 2585 | preview: None, |
| 2436 | recording: None, | 2586 | recording: None, |
| ... | @@ -2542,7 +2692,10 @@ mod tests { | ... | @@ -2542,7 +2692,10 @@ mod tests { |
| 2542 | .iter() | 2692 | .iter() |
| 2543 | .map(|tag| (tag.icon, tag.origin[0])) | 2693 | .map(|tag| (tag.icon, tag.origin[0])) |
| 2544 | .collect::<Vec<_>>(), | 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 | let colors: Vec<_> = shaped | 2700 | let colors: Vec<_> = shaped |
| 2548 | .text | 2701 | .text |
crates/canvas/src/template.rs+30| ... | @@ -21,6 +21,36 @@ pub struct Template { | ... | @@ -21,6 +21,36 @@ pub struct Template { |
| 21 | pub art: &'static [Art], | 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 | /// OneNote 2010's Decorative templates and Informal Meeting Notes (resources/onenote-templates.md), | 54 | /// OneNote 2010's Decorative templates and Informal Meeting Notes (resources/onenote-templates.md), |
| 25 | /// in the order the gallery lists them. | 55 | /// in the order the gallery lists them. |
| 26 | pub const TEMPLATES: &[Template] = &[ | 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,12 +13,21 @@ use canvas::{ |
| 13 | use draw::edit::Movement; | 13 | use draw::edit::Movement; |
| 14 | use onestore::{ | 14 | use onestore::{ |
| 15 | Arena, ExGuid, RevisionIndex, Section, Store, | 15 | Arena, ExGuid, RevisionIndex, Section, Store, |
| 16 | document::{Document, Kind as Node}, | 16 | document::{Document, Kind as Node, Layout}, |
| 17 | op::{Edit, Op, PageOp}, | 17 | op::{Edit, Op, PageOp}, |
| 18 | page::Page, | 18 | page::{Image, Page}, |
| 19 | }; | 19 | }; |
| 20 | use std::{collections::BTreeMap, path::Path}; | 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 | #[path = "../../onestore/tests/support/sweep.rs"] | 31 | #[path = "../../onestore/tests/support/sweep.rs"] |
| 23 | mod sweep; | 32 | mod sweep; |
| 24 | 33 | ||
| ... | @@ -98,11 +107,22 @@ enum Kind { | ... | @@ -98,11 +107,22 @@ enum Kind { |
| 98 | InsertSpace, | 107 | InsertSpace, |
| 99 | RemoveSpace, | 108 | RemoveSpace, |
| 100 | InsertSpaceRight, | 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 | Undo, | 121 | Undo, |
| 102 | Redo, | 122 | Redo, |
| 103 | } | 123 | } |
| 104 | 124 | ||
| 105 | const KINDS: [Kind; 55] = [ | 125 | const KINDS: [Kind; 57] = [ |
| 106 | Kind::Type, | 126 | Kind::Type, |
| 107 | Kind::TypeEnd, | 127 | Kind::TypeEnd, |
| 108 | Kind::Compose, | 128 | Kind::Compose, |
| ... | @@ -156,10 +176,61 @@ const KINDS: [Kind; 55] = [ | ... | @@ -156,10 +176,61 @@ const KINDS: [Kind; 55] = [ |
| 156 | Kind::InsertSpace, | 176 | Kind::InsertSpace, |
| 157 | Kind::RemoveSpace, | 177 | Kind::RemoveSpace, |
| 158 | Kind::InsertSpaceRight, | 178 | Kind::InsertSpaceRight, |
| 179 | Kind::Paper, | ||
| 180 | Kind::Art, | ||
| 159 | Kind::Undo, | 181 | Kind::Undo, |
| 160 | Kind::Redo, | 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 | struct Random(u64); | 234 | struct Random(u64); |
| 164 | 235 | ||
| 165 | impl Random { | 236 | impl Random { |
| ... | @@ -212,10 +283,13 @@ fn perform( | ... | @@ -212,10 +283,13 @@ fn perform( |
| 212 | kind, | 283 | kind, |
| 213 | Kind::Undo | 284 | Kind::Undo |
| 214 | | Kind::Redo | 285 | | Kind::Redo |
| 286 | | Kind::TypeOn | ||
| 215 | | Kind::PlaceImage | 287 | | Kind::PlaceImage |
| 216 | | Kind::RemoveImage | 288 | | Kind::RemoveImage |
| 217 | | Kind::CreateOutline | 289 | | Kind::CreateOutline |
| 218 | | Kind::Date | 290 | | Kind::Date |
| 291 | | Kind::Paper | ||
| 292 | | Kind::Art | ||
| 219 | ) { | 293 | ) { |
| 220 | if outlines.is_empty() { | 294 | if outlines.is_empty() { |
| 221 | return false; | 295 | return false; |
| ... | @@ -328,18 +402,9 @@ fn perform( | ... | @@ -328,18 +402,9 @@ fn perform( |
| 328 | Kind::Bullets => format(editor, engine, random, Formatting::Bullets), | 402 | Kind::Bullets => format(editor, engine, random, Formatting::Bullets), |
| 329 | Kind::Numbering => format(editor, engine, random, Formatting::Numbering), | 403 | Kind::Numbering => format(editor, engine, random, Formatting::Numbering), |
| 330 | Kind::Tag => { | 404 | Kind::Tag => { |
| 331 | let tag = [ | 405 | let place = random.below(9); |
| 332 | NoteTag::ToDo, | 406 | let tag = NoteTag::defaults().swap_remove(place); |
| 333 | NoteTag::Important, | 407 | format(editor, engine, random, Formatting::Tag(tag, place as u16)) |
| 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)) | ||
| 343 | } | 408 | } |
| 344 | Kind::Check => format(editor, engine, random, Formatting::Check), | 409 | Kind::Check => format(editor, engine, random, Formatting::Check), |
| 345 | Kind::ClickCheck => { | 410 | Kind::ClickCheck => { |
| ... | @@ -462,6 +527,106 @@ fn perform( | ... | @@ -462,6 +527,106 @@ fn perform( |
| 462 | let line = editor.active_outline().origin()[0] - random.below(40) as f32; | 527 | let line = editor.active_outline().origin()[0] - random.below(40) as f32; |
| 463 | editor.insert_space(engine, 0, line, 45.0).unwrap_or(false) | 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 | Kind::Undo => editor.undo(engine).unwrap_or(false), | 630 | Kind::Undo => editor.undo(engine).unwrap_or(false), |
| 466 | Kind::Redo => editor.redo(engine).unwrap_or(false), | 631 | Kind::Redo => editor.redo(engine).unwrap_or(false), |
| 467 | } | 632 | } |
| ... | @@ -749,6 +914,10 @@ fn sweep( | ... | @@ -749,6 +914,10 @@ fn sweep( |
| 749 | .map(|_| KINDS[random.below(KINDS.len())]) | 914 | .map(|_| KINDS[random.below(KINDS.len())]) |
| 750 | .collect() | 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 | .collect::<Vec<Vec<Kind>>>(); | 921 | .collect::<Vec<Vec<Kind>>>(); |
| 753 | for plan in plans { | 922 | for plan in plans { |
| 754 | let arena = Arena::default(); | 923 | let arena = Arena::default(); |
crates/canvas/tests/search.rs+2-1| ... | @@ -235,7 +235,8 @@ fn tags_summary_lists_each_tagged_paragraph() { | ... | @@ -235,7 +235,8 @@ fn tags_summary_lists_each_tagged_paragraph() { |
| 235 | // The probe's T24 and T25 also carry Call back, one entry under each tag. | 235 | // The probe's T24 and T25 also carry Call back, one entry under each tag. |
| 236 | assert_eq!(tagged.len(), names.len()); | 236 | assert_eq!(tagged.len(), names.len()); |
| 237 | names.dedup(); | 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 | labels.sort_unstable(); | 240 | labels.sort_unstable(); |
| 240 | assert_eq!(names, labels); | 241 | assert_eq!(names, labels); |
| 241 | assert!(tagged.iter().all(|tagged| tagged.space == space)); | 242 | assert!(tagged.iter().all(|tagged| tagged.space == space)); |
crates/draw/src/draw.glsl+31-1| ... | @@ -10,6 +10,7 @@ attribute vec4 shape; | ... | @@ -10,6 +10,7 @@ attribute vec4 shape; |
| 10 | attribute float stroke; | 10 | attribute float stroke; |
| 11 | attribute vec2 clip_local; | 11 | attribute vec2 clip_local; |
| 12 | attribute vec3 clip; | 12 | attribute vec3 clip; |
| 13 | attribute float blur; | ||
| 13 | #endif | 14 | #endif |
| 14 | varying vec2 v_uv; | 15 | varying vec2 v_uv; |
| 15 | varying vec4 v_color; | 16 | varying vec4 v_color; |
| ... | @@ -18,6 +19,7 @@ varying vec4 v_shape; | ... | @@ -18,6 +19,7 @@ varying vec4 v_shape; |
| 18 | varying float v_stroke; | 19 | varying float v_stroke; |
| 19 | varying vec2 v_clip_local; | 20 | varying vec2 v_clip_local; |
| 20 | varying vec3 v_clip; | 21 | varying vec3 v_clip; |
| 22 | varying float v_blur; | ||
| 21 | 23 | ||
| 22 | #ifdef VERTEX | 24 | #ifdef VERTEX |
| 23 | void main() { | 25 | void main() { |
| ... | @@ -28,6 +30,7 @@ void main() { | ... | @@ -28,6 +30,7 @@ void main() { |
| 28 | v_stroke = stroke; | 30 | v_stroke = stroke; |
| 29 | v_clip_local = clip_local; | 31 | v_clip_local = clip_local; |
| 30 | v_clip = clip; | 32 | v_clip = clip; |
| 33 | v_blur = blur; | ||
| 31 | gl_Position = vec4(position, 0.0, 1.0); | 34 | gl_Position = vec4(position, 0.0, 1.0); |
| 32 | } | 35 | } |
| 33 | #else | 36 | #else |
| ... | @@ -54,10 +57,37 @@ float ellipse_arc(float angle, vec2 radius) { | ... | @@ -54,10 +57,37 @@ float ellipse_arc(float angle, vec2 radius) { |
| 54 | return half_angle * sum * scale; | 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 | void main() { | 85 | void main() { |
| 58 | vec4 color = texture2D(atlas, v_uv) * v_color; | 86 | vec4 color = texture2D(atlas, v_uv) * v_color; |
| 59 | vec4 shape = v_shape; | 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 | vec2 q = abs(v_local) - shape.xy + shape.zw; | 91 | vec2 q = abs(v_local) - shape.xy + shape.zw; |
| 62 | float distance; | 92 | float distance; |
| 63 | if (same(shape.z, shape.w)) { | 93 | if (same(shape.z, shape.w)) { |
crates/draw/src/draw.wgsl+35-3| ... | @@ -7,6 +7,7 @@ struct Vertex { | ... | @@ -7,6 +7,7 @@ struct Vertex { |
| 7 | @location(4) @interpolate(flat) stroke: f32, | 7 | @location(4) @interpolate(flat) stroke: f32, |
| 8 | @location(5) clip_local: vec2<f32>, | 8 | @location(5) clip_local: vec2<f32>, |
| 9 | @location(6) @interpolate(flat) clip: vec3<f32>, | 9 | @location(6) @interpolate(flat) clip: vec3<f32>, |
| 10 | @location(7) @interpolate(flat) blur: f32, | ||
| 10 | } | 11 | } |
| 11 | 12 | ||
| 12 | @group(0) @binding(0) var atlas: texture_2d<f32>; | 13 | @group(0) @binding(0) var atlas: texture_2d<f32>; |
| ... | @@ -16,8 +17,9 @@ struct Vertex { | ... | @@ -16,8 +17,9 @@ struct Vertex { |
| 16 | fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>, | 17 | fn vertex(@location(0) position: vec2<f32>, @location(1) uv: vec2<f32>, |
| 17 | @location(2) color: vec4<f32>, @location(3) local: vec2<f32>, | 18 | @location(2) color: vec4<f32>, @location(3) local: vec2<f32>, |
| 18 | @location(4) shape: vec4<f32>, @location(5) stroke: f32, | 19 | @location(4) shape: vec4<f32>, @location(5) stroke: f32, |
| 19 | @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>) -> Vertex { | 20 | @location(6) clip_local: vec2<f32>, @location(7) clip: vec3<f32>, |
| 20 | return Vertex(vec4<f32>(position, 0.0, 1.0), uv, color, local, shape, stroke, clip_local, clip); | 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 | fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { | 25 | fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { |
| ... | @@ -37,10 +39,40 @@ fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { | ... | @@ -37,10 +39,40 @@ fn ellipse_arc(angle: f32, radius: vec2<f32>) -> f32 { |
| 37 | return half * sum * scale; | 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 | @fragment | 70 | @fragment |
| 41 | fn fragment(input: Vertex) -> @location(0) vec4<f32> { | 71 | fn fragment(input: Vertex) -> @location(0) vec4<f32> { |
| 42 | var color = textureSample(atlas, atlas_sampler, input.uv) * input.color; | 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 | let q = abs(input.local) - input.shape.xy + input.shape.zw; | 76 | let q = abs(input.local) - input.shape.xy + input.shape.zw; |
| 45 | var distance: f32; | 77 | var distance: f32; |
| 46 | if input.shape.z == input.shape.w { | 78 | if input.shape.z == input.shape.w { |
crates/draw/src/render.rs+184-41| ... | @@ -32,7 +32,11 @@ use swash::{ | ... | @@ -32,7 +32,11 @@ use swash::{ |
| 32 | zeno::{Angle, Cap, Format, Join, Mask, Transform, Vector}, | 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 | const ATLAS_SIZE: u32 = 2048; | 37 | const ATLAS_SIZE: u32 = 2048; |
| 38 | const MAX_ATLAS_SIZE: u32 = 8192; | ||
| 39 | /// Atlas entries per `ATLAS_SIZE` square of atlas. | ||
| 36 | const MAX_GLYPHS: usize = 8192; | 40 | const MAX_GLYPHS: usize = 8192; |
| 37 | const MAX_VERTICES: usize = 65_536; | 41 | const MAX_VERTICES: usize = 65_536; |
| 38 | const VERTEX_BUFFER_BYTES: u64 = (MAX_VERTICES * size_of::<Vertex>()) as u64; | 42 | const VERTEX_BUFFER_BYTES: u64 = (MAX_VERTICES * size_of::<Vertex>()) as u64; |
| ... | @@ -199,6 +203,8 @@ struct Vertex { | ... | @@ -199,6 +203,8 @@ struct Vertex { |
| 199 | /// that rectangle's half size and corner radius; a zero half size paints everywhere. | 203 | /// that rectangle's half size and corner radius; a zero half size paints everywhere. |
| 200 | clip_local: [f32; 2], | 204 | clip_local: [f32; 2], |
| 201 | clip: [f32; 3], | 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 | #[derive(Hash, PartialEq, Eq)] | 210 | #[derive(Hash, PartialEq, Eq)] |
| ... | @@ -401,6 +407,14 @@ pub enum Primitive<'a> { | ... | @@ -401,6 +407,14 @@ pub enum Primitive<'a> { |
| 401 | radius: [f32; 2], | 407 | radius: [f32; 2], |
| 402 | colors: [[f32; 4]; 2], | 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 | Image { | 418 | Image { |
| 405 | image: &'a RasterImage, | 419 | image: &'a RasterImage, |
| 406 | rect: [f32; 4], | 420 | rect: [f32; 4], |
| ... | @@ -490,11 +504,36 @@ impl Renderer { | ... | @@ -490,11 +504,36 @@ impl Renderer { |
| 490 | pen: [1, 0], | 504 | pen: [1, 0], |
| 491 | row_height: 1, | 505 | row_height: 1, |
| 492 | }; | 506 | }; |
| 493 | // The atlas's first texel is the white that solid fills sample. | ||
| 494 | renderer.write_atlas([0, 0], [1, 1], &[255; 4]); | 507 | renderer.write_atlas([0, 0], [1, 1], &[255; 4]); |
| 495 | renderer | 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 | pub fn clear_glyph_cache(&mut self) { | 537 | pub fn clear_glyph_cache(&mut self) { |
| 499 | self.glyphs.clear(); | 538 | self.glyphs.clear(); |
| 500 | self.pen = [1, 0]; | 539 | self.pen = [1, 0]; |
| ... | @@ -524,9 +563,9 @@ impl Renderer { | ... | @@ -524,9 +563,9 @@ impl Renderer { |
| 524 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), | 563 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), |
| 525 | glyph_capacity: self.glyphs.capacity(), | 564 | glyph_capacity: self.glyphs.capacity(), |
| 526 | image_capacity: self.images.capacity(), | 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 | vertex_buffer_bytes: VERTEX_BUFFER_BYTES, | 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 | && self.images.len() <= MAX_IMAGES | 569 | && self.images.len() <= MAX_IMAGES |
| 531 | && image_bytes <= MAX_IMAGE_BYTES | 570 | && image_bytes <= MAX_IMAGE_BYTES |
| 532 | && self.vertices.len() <= MAX_VERTICES, | 571 | && self.vertices.len() <= MAX_VERTICES, |
| ... | @@ -578,17 +617,35 @@ impl Renderer { | ... | @@ -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 | self.vertices.clear(); | 623 | self.vertices.clear(); |
| 583 | self.batches.clear(); | 624 | self.batches.clear(); |
| 584 | self.groups.clear(); | 625 | self.groups.clear(); |
| 585 | self.barrier = 0; | 626 | self.barrier = 0; |
| 586 | match self.prepare(size, layers, &active_images) { | 627 | let full = match self.prepare(size, layers, &active_images) { |
| 587 | Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(), | 628 | Err(RenderError::AtlasFull) => true, |
| 588 | result => { | 629 | result => { |
| 589 | result?; | 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 | self.submit(target, size, clear); | 651 | self.submit(target, size, clear); |
| ... | @@ -677,6 +734,7 @@ impl Renderer { | ... | @@ -677,6 +734,7 @@ impl Renderer { |
| 677 | stroke: 0.0, | 734 | stroke: 0.0, |
| 678 | clip_local: [0.0; 2], | 735 | clip_local: [0.0; 2], |
| 679 | clip: [0.0; 3], | 736 | clip: [0.0; 3], |
| 737 | blur: 0.0, | ||
| 680 | } | 738 | } |
| 681 | }; | 739 | }; |
| 682 | for strip in 0..strips { | 740 | for strip in 0..strips { |
| ... | @@ -766,7 +824,7 @@ impl Renderer { | ... | @@ -766,7 +824,7 @@ impl Renderer { |
| 766 | rect[2] * space.scale + space.origin[0], | 824 | rect[2] * space.scale + space.origin[0], |
| 767 | rect[3] * space.scale + space.origin[1], | 825 | rect[3] * space.scale + space.origin[1], |
| 768 | ], | 826 | ], |
| 769 | [0.5 / ATLAS_SIZE as f32; 4], | 827 | self.white(), |
| 770 | *color, | 828 | *color, |
| 771 | )?; | 829 | )?; |
| 772 | } | 830 | } |
| ... | @@ -781,6 +839,12 @@ impl Renderer { | ... | @@ -781,6 +839,12 @@ impl Renderer { |
| 781 | radius, | 839 | radius, |
| 782 | colors, | 840 | colors, |
| 783 | } => self.rounded_rect(space, *rect, *radius, None, *colors)?, | 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 | Primitive::Segment { | 848 | Primitive::Segment { |
| 785 | from, | 849 | from, |
| 786 | to, | 850 | to, |
| ... | @@ -871,7 +935,7 @@ impl Renderer { | ... | @@ -871,7 +935,7 @@ impl Renderer { |
| 871 | return Ok(()); | 935 | return Ok(()); |
| 872 | } | 936 | } |
| 873 | let size = run.size * scale; | 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 | return Err(RenderError::AtlasFull); | 939 | return Err(RenderError::AtlasFull); |
| 876 | } | 940 | } |
| 877 | let color = run | 941 | let color = run |
| ... | @@ -905,7 +969,7 @@ impl Renderer { | ... | @@ -905,7 +969,7 @@ impl Renderer { |
| 905 | let cached = if let Some(cached) = self.glyphs.get(&key) { | 969 | let cached = if let Some(cached) = self.glyphs.get(&key) { |
| 906 | *cached | 970 | *cached |
| 907 | } else { | 971 | } else { |
| 908 | if self.glyphs.len() >= MAX_GLYPHS { | 972 | if self.glyphs.len() >= self.glyph_limit() { |
| 909 | return Err(RenderError::AtlasFull); | 973 | return Err(RenderError::AtlasFull); |
| 910 | } | 974 | } |
| 911 | let font = FontRef::from_index(run.font.as_ref(), run.index as usize) | 975 | let font = FontRef::from_index(run.font.as_ref(), run.index as usize) |
| ... | @@ -967,16 +1031,10 @@ impl Renderer { | ... | @@ -967,16 +1031,10 @@ impl Renderer { |
| 967 | height *= ratio; | 1031 | height *= ratio; |
| 968 | top = top.clamp(line_top, (line_top + line_height - height).max(line_top)); | 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 | self.quad( | 1034 | self.quad( |
| 972 | space, | 1035 | space, |
| 973 | [left, top, left + width, top + height], | 1036 | [left, top, left + width, top + height], |
| 974 | [ | 1037 | self.uv(glyph), |
| 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 | ], | ||
| 980 | if glyph.color { [1.0; 4] } else { color }, | 1038 | if glyph.color { [1.0; 4] } else { color }, |
| 981 | )?; | 1039 | )?; |
| 982 | } | 1040 | } |
| ... | @@ -992,7 +1050,7 @@ impl Renderer { | ... | @@ -992,7 +1050,7 @@ impl Renderer { |
| 992 | x + decoration.width * scale, | 1050 | x + decoration.width * scale, |
| 993 | y + (decoration.thickness * scale).max(1.0), | 1051 | y + (decoration.thickness * scale).max(1.0), |
| 994 | ], | 1052 | ], |
| 995 | [0.5 / ATLAS_SIZE as f32; 4], | 1053 | self.white(), |
| 996 | decoration | 1054 | decoration |
| 997 | .color | 1055 | .color |
| 998 | .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)), | 1056 | .map_or(ink, |color| legible(color, space.backdrop, run.backdrop)), |
| ... | @@ -1003,15 +1061,16 @@ impl Renderer { | ... | @@ -1003,15 +1061,16 @@ impl Renderer { |
| 1003 | 1061 | ||
| 1004 | fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> { | 1062 | fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> { |
| 1005 | let p = image.placement; | 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 | return Err(RenderError::AtlasFull); | 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 | self.pen[0] = 0; | 1069 | self.pen[0] = 0; |
| 1011 | self.pen[1] += self.row_height + 1; | 1070 | self.pen[1] += self.row_height + 1; |
| 1012 | self.row_height = 0; | 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 | return Err(RenderError::AtlasFull); | 1074 | return Err(RenderError::AtlasFull); |
| 1016 | } | 1075 | } |
| 1017 | let cached = AtlasGlyph { | 1076 | let cached = AtlasGlyph { |
| ... | @@ -1057,7 +1116,7 @@ impl Renderer { | ... | @@ -1057,7 +1116,7 @@ impl Renderer { |
| 1057 | { | 1116 | { |
| 1058 | return Ok(()); | 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 | return Err(RenderError::AtlasFull); | 1120 | return Err(RenderError::AtlasFull); |
| 1062 | } | 1121 | } |
| 1063 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | 1122 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; |
| ... | @@ -1072,7 +1131,7 @@ impl Renderer { | ... | @@ -1072,7 +1131,7 @@ impl Renderer { |
| 1072 | let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) { | 1131 | let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) { |
| 1073 | *glyph | 1132 | *glyph |
| 1074 | } else { | 1133 | } else { |
| 1075 | if self.glyphs.len() >= MAX_GLYPHS { | 1134 | if self.glyphs.len() >= self.glyph_limit() { |
| 1076 | return Err(RenderError::AtlasFull); | 1135 | return Err(RenderError::AtlasFull); |
| 1077 | } | 1136 | } |
| 1078 | let glyph = self.upload(icon::rasterize(sources, size, phase, ink, palette))?; | 1137 | let glyph = self.upload(icon::rasterize(sources, size, phase, ink, palette))?; |
| ... | @@ -1081,7 +1140,6 @@ impl Renderer { | ... | @@ -1081,7 +1140,6 @@ impl Renderer { |
| 1081 | }; | 1140 | }; |
| 1082 | let left = x.floor(); | 1141 | let left = x.floor(); |
| 1083 | let top = y.floor(); | 1142 | let top = y.floor(); |
| 1084 | let atlas = ATLAS_SIZE as f32; | ||
| 1085 | self.quad( | 1143 | self.quad( |
| 1086 | space, | 1144 | space, |
| 1087 | [ | 1145 | [ |
| ... | @@ -1090,12 +1148,7 @@ impl Renderer { | ... | @@ -1090,12 +1148,7 @@ impl Renderer { |
| 1090 | left + glyph.width as f32, | 1148 | left + glyph.width as f32, |
| 1091 | top + glyph.height as f32, | 1149 | top + glyph.height as f32, |
| 1092 | ], | 1150 | ], |
| 1093 | [ | 1151 | self.uv(glyph), |
| 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 | ], | ||
| 1099 | [1.0, 1.0, 1.0, tint[3]], | 1152 | [1.0, 1.0, 1.0, tint[3]], |
| 1100 | ) | 1153 | ) |
| 1101 | } | 1154 | } |
| ... | @@ -1133,7 +1186,7 @@ impl Renderer { | ... | @@ -1133,7 +1186,7 @@ impl Renderer { |
| 1133 | let start = self.vertices.len(); | 1186 | let start = self.vertices.len(); |
| 1134 | // A pixel of margin holds the antialiased fringe outside the edge. | 1187 | // A pixel of margin holds the antialiased fringe outside the edge. |
| 1135 | let fringe = [rect[0] - 1.0, rect[1] - 1.0, rect[2] + 1.0, rect[3] + 1.0]; | 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 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | 1190 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; |
| 1138 | if !(4.0 * (half[0] + half[1])).is_finite() || !colors[1].iter().all(|v| v.is_finite()) { | 1191 | if !(4.0 * (half[0] + half[1])).is_finite() || !colors[1].iter().all(|v| v.is_finite()) { |
| 1139 | return Err(RenderError::InvalidPrimitive); | 1192 | return Err(RenderError::InvalidPrimitive); |
| ... | @@ -1160,6 +1213,41 @@ impl Renderer { | ... | @@ -1160,6 +1213,41 @@ impl Renderer { |
| 1160 | Ok(()) | 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 | fn path( | 1251 | fn path( |
| 1164 | &mut self, | 1252 | &mut self, |
| 1165 | space: Space, | 1253 | space: Space, |
| ... | @@ -1195,7 +1283,7 @@ impl Renderer { | ... | @@ -1195,7 +1283,7 @@ impl Renderer { |
| 1195 | let glyph = match self.glyphs.get(&key) { | 1283 | let glyph = match self.glyphs.get(&key) { |
| 1196 | Some(glyph) => *glyph, | 1284 | Some(glyph) => *glyph, |
| 1197 | None => { | 1285 | None => { |
| 1198 | if self.glyphs.len() >= MAX_GLYPHS { | 1286 | if self.glyphs.len() >= self.glyph_limit() { |
| 1199 | return Err(RenderError::AtlasFull); | 1287 | return Err(RenderError::AtlasFull); |
| 1200 | } | 1288 | } |
| 1201 | let mut mask = Mask::new(data); | 1289 | let mut mask = Mask::new(data); |
| ... | @@ -1232,7 +1320,6 @@ impl Renderer { | ... | @@ -1232,7 +1320,6 @@ impl Renderer { |
| 1232 | }; | 1320 | }; |
| 1233 | let left = x.floor() + glyph.left as f32; | 1321 | let left = x.floor() + glyph.left as f32; |
| 1234 | let top = y.floor() + glyph.top as f32; | 1322 | let top = y.floor() + glyph.top as f32; |
| 1235 | let atlas = ATLAS_SIZE as f32; | ||
| 1236 | let start = self.vertices.len(); | 1323 | let start = self.vertices.len(); |
| 1237 | self.quad( | 1324 | self.quad( |
| 1238 | space, | 1325 | space, |
| ... | @@ -1242,12 +1329,7 @@ impl Renderer { | ... | @@ -1242,12 +1329,7 @@ impl Renderer { |
| 1242 | left + glyph.width as f32, | 1329 | left + glyph.width as f32, |
| 1243 | top + glyph.height as f32, | 1330 | top + glyph.height as f32, |
| 1244 | ], | 1331 | ], |
| 1245 | [ | 1332 | self.uv(glyph), |
| 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 | ], | ||
| 1251 | colors[0], | 1333 | colors[0], |
| 1252 | )?; | 1334 | )?; |
| 1253 | for vertex in &mut self.vertices[start..] { | 1335 | for vertex in &mut self.vertices[start..] { |
| ... | @@ -1318,13 +1400,14 @@ impl Renderer { | ... | @@ -1318,13 +1400,14 @@ impl Renderer { |
| 1318 | x * 2.0 / space.size[0] as f32 - 1.0, | 1400 | x * 2.0 / space.size[0] as f32 - 1.0, |
| 1319 | 1.0 - y * 2.0 / space.size[1] as f32, | 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 | color, | 1404 | color, |
| 1323 | local, | 1405 | local, |
| 1324 | shape: [half[0], half[1], corner, corner], | 1406 | shape: [half[0], half[1], corner, corner], |
| 1325 | stroke: 0.0, | 1407 | stroke: 0.0, |
| 1326 | clip_local: [0.0; 2], | 1408 | clip_local: [0.0; 2], |
| 1327 | clip: [0.0; 3], | 1409 | clip: [0.0; 3], |
| 1410 | blur: 0.0, | ||
| 1328 | }); | 1411 | }); |
| 1329 | } | 1412 | } |
| 1330 | Ok(()) | 1413 | Ok(()) |
| ... | @@ -1378,6 +1461,7 @@ impl Renderer { | ... | @@ -1378,6 +1461,7 @@ impl Renderer { |
| 1378 | stroke: 0.0, | 1461 | stroke: 0.0, |
| 1379 | clip_local: [0.0; 2], | 1462 | clip_local: [0.0; 2], |
| 1380 | clip: [0.0; 3], | 1463 | clip: [0.0; 3], |
| 1464 | blur: 0.0, | ||
| 1381 | }); | 1465 | }); |
| 1382 | } | 1466 | } |
| 1383 | Ok(()) | 1467 | Ok(()) |
| ... | @@ -2031,7 +2115,7 @@ mod tests { | ... | @@ -2031,7 +2115,7 @@ mod tests { |
| 2031 | ); | 2115 | ); |
| 2032 | } | 2116 | } |
| 2033 | assert!(!renderer.glyphs.is_empty()); | 2117 | assert!(!renderer.glyphs.is_empty()); |
| 2034 | assert!(renderer.glyphs.len() <= MAX_GLYPHS); | 2118 | assert!(renderer.glyphs.len() <= renderer.glyph_limit()); |
| 2035 | assert_eq!( | 2119 | assert_eq!( |
| 2036 | renderer | 2120 | renderer |
| 2037 | .glyphs | 2121 | .glyphs |
| ... | @@ -2255,6 +2339,12 @@ mod tests { | ... | @@ -2255,6 +2339,12 @@ mod tests { |
| 2255 | style: PathStyle::Shadow(6.0), | 2339 | style: PathStyle::Shadow(6.0), |
| 2256 | colors: [[0.0, 0.0, 0.0, 1.0]; 2], | 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 | Primitive::Gradient { | 2348 | Primitive::Gradient { |
| 2259 | rect: [100.0, 10.0, 120.0, 110.0], | 2349 | rect: [100.0, 10.0, 120.0, 110.0], |
| 2260 | radius: [0.0; 2], | 2350 | radius: [0.0; 2], |
| ... | @@ -2305,6 +2395,59 @@ mod tests { | ... | @@ -2305,6 +2395,59 @@ mod tests { |
| 2305 | "and fades at its edge: {fading:?}" | 2395 | "and fades at its edge: {fading:?}" |
| 2306 | ); | 2396 | ); |
| 2307 | assert_eq!(beyond, [255; 4]); | 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 | #[test] | 2453 | #[test] |
crates/draw/src/render/gl.rs+10-1| ... | @@ -305,7 +305,7 @@ pub(super) struct Gpu { | ... | @@ -305,7 +305,7 @@ pub(super) struct Gpu { |
| 305 | } | 305 | } |
| 306 | 306 | ||
| 307 | /// Each attribute's name, component count and offset, bound to its index in the shader. | 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 | (c"position", 2, offset_of!(Vertex, position)), | 309 | (c"position", 2, offset_of!(Vertex, position)), |
| 310 | (c"uv", 2, offset_of!(Vertex, uv)), | 310 | (c"uv", 2, offset_of!(Vertex, uv)), |
| 311 | (c"color", 4, offset_of!(Vertex, color)), | 311 | (c"color", 4, offset_of!(Vertex, color)), |
| ... | @@ -314,6 +314,7 @@ const ATTRIBUTES: [(&CStr, GLint, usize); 8] = [ | ... | @@ -314,6 +314,7 @@ const ATTRIBUTES: [(&CStr, GLint, usize); 8] = [ |
| 314 | (c"stroke", 1, offset_of!(Vertex, stroke)), | 314 | (c"stroke", 1, offset_of!(Vertex, stroke)), |
| 315 | (c"clip_local", 2, offset_of!(Vertex, clip_local)), | 315 | (c"clip_local", 2, offset_of!(Vertex, clip_local)), |
| 316 | (c"clip", 3, offset_of!(Vertex, clip)), | 316 | (c"clip", 3, offset_of!(Vertex, clip)), |
| 317 | (c"blur", 1, offset_of!(Vertex, blur)), | ||
| 317 | ]; | 318 | ]; |
| 318 | 319 | ||
| 319 | impl Renderer { | 320 | impl Renderer { |
| ... | @@ -384,6 +385,14 @@ impl Renderer { | ... | @@ -384,6 +385,14 @@ impl Renderer { |
| 384 | self.gpu.max_texture | 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 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { | 396 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { |
| 388 | unsafe { | 397 | unsafe { |
| 389 | glBindTexture(TEXTURE_2D, self.gpu.atlas.name); | 398 | glBindTexture(TEXTURE_2D, self.gpu.atlas.name); |
crates/draw/src/render/webgpu.rs+53-35| ... | @@ -63,7 +63,7 @@ impl Renderer { | ... | @@ -63,7 +63,7 @@ impl Renderer { |
| 63 | buffers: &[Some(wgpu::VertexBufferLayout { | 63 | buffers: &[Some(wgpu::VertexBufferLayout { |
| 64 | array_stride: size_of::<Vertex>() as u64, | 64 | array_stride: size_of::<Vertex>() as u64, |
| 65 | step_mode: wgpu::VertexStepMode::Vertex, | 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 | fragment: Some(wgpu::FragmentState { | 69 | fragment: Some(wgpu::FragmentState { |
| ... | @@ -98,40 +98,7 @@ impl Renderer { | ... | @@ -98,40 +98,7 @@ impl Renderer { |
| 98 | color: erase, | 98 | color: erase, |
| 99 | alpha: erase, | 99 | alpha: erase, |
| 100 | }); | 100 | }); |
| 101 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | 101 | let (atlas, bind_group) = atlas(&device, &pipeline, ATLAS_SIZE); |
| 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 | }); | ||
| 135 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { | 102 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { |
| 136 | label: Some("Draw vertices"), | 103 | label: Some("Draw vertices"), |
| 137 | size: VERTEX_BUFFER_BYTES, | 104 | size: VERTEX_BUFFER_BYTES, |
| ... | @@ -167,6 +134,14 @@ impl Renderer { | ... | @@ -167,6 +134,14 @@ impl Renderer { |
| 167 | self.device.limits().max_texture_dimension_2d | 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 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { | 145 | pub(super) fn write_atlas(&self, origin: [u32; 2], size: [u32; 2], rgba: &[u8]) { |
| 171 | self.queue.write_texture( | 146 | self.queue.write_texture( |
| 172 | wgpu::TexelCopyTextureInfo { | 147 | wgpu::TexelCopyTextureInfo { |
| ... | @@ -336,3 +311,46 @@ impl Renderer { | ... | @@ -336,3 +311,46 @@ impl Renderer { |
| 336 | self.queue.submit([encoder.finish()]); | 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,6 +16,7 @@ onestore = { path = "../onestore" } |
| 16 | wgpu.workspace = true | 16 | wgpu.workspace = true |
| 17 | parley.workspace = true | 17 | parley.workspace = true |
| 18 | pollster = "0.4" | 18 | pollster = "0.4" |
| 19 | resvg = { version = "0.48.1", default-features = false } | ||
| 19 | serde = { version = "1", features = ["derive"] } | 20 | serde = { version = "1", features = ["derive"] } |
| 20 | serde_json = "1" | 21 | serde_json = "1" |
| 21 | 22 |
crates/mobile/include/snowbound.h+14-1| ... | @@ -14,6 +14,9 @@ void sb_string_free(char *text); | ... | @@ -14,6 +14,9 @@ void sb_string_free(char *text); |
| 14 | 14 | ||
| 15 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); | 15 | typedef void (*sb_coordinator)(const char *path, bool write, void (*body)(void *), void *context); |
| 16 | void sb_set_coordinator(sb_coordinator coordinator); | 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 | const Library *sb_library_open(const char *path, const char *cache, char **error); | 21 | const Library *sb_library_open(const char *path, const char *cache, char **error); |
| 19 | const Library *sb_library_server(const char *address, const char *share, const char *user, const char *password, | 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,6 +25,10 @@ char *sb_library_sections(const Library *library); |
| 22 | char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author, | 25 | char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author, |
| 23 | const char *date, const char *time); | 26 | const char *date, const char *time); |
| 24 | char *sb_library_search(const Library *library, const Section *open, const char *path, const char *query); | 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 | void sb_library_free(const Library *library); | 32 | void sb_library_free(const Library *library); |
| 26 | 33 | ||
| 27 | Share *sb_share_connect(const char *address, const char *share, const char *user, const char *password, | 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,11 +65,17 @@ bool sb_view_drag(View *view, float x, float y); |
| 58 | bool sb_view_release(View *view); | 65 | bool sb_view_release(View *view); |
| 59 | bool sb_view_undo(View *view, bool redo); | 66 | bool sb_view_undo(View *view, bool redo); |
| 60 | bool sb_view_can_undo(const View *view, bool redo); | 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 | bool sb_view_apply(View *view, uint8_t command); | 69 | bool sb_view_apply(View *view, uint8_t command); |
| 63 | char *sb_view_title(const View *view); | 70 | char *sb_view_title(const View *view); |
| 64 | bool sb_view_focus_title(View *view); | 71 | bool sb_view_focus_title(View *view); |
| 65 | bool sb_view_find(View *view, const char *query); | 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 | char *sb_view_page_text(const View *view); | 79 | char *sb_view_page_text(const View *view); |
| 67 | int8_t sb_view_date_request(View *view, int64_t *seconds); | 80 | int8_t sb_view_date_request(View *view, int64_t *seconds); |
| 68 | bool sb_view_change_date(View *view, int64_t seconds, const char *date, const char *time); | 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,9 +78,9 @@ enum Target { |
| 78 | Grip = 3, | 78 | Grip = 3, |
| 79 | } | 79 | } |
| 80 | 80 | ||
| 81 | /// Formatting the toolbar shows and applies, by bit in `sb_view_format` and by number in | 81 | /// Formatting the host 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. | 82 | /// `sb_view_apply`; OneNote's default tags follow from 16, in their list's order. |
| 83 | const FORMATS: [Formatting; 8] = [ | 83 | const FORMATS: [Formatting; 11] = [ |
| 84 | Formatting::Toggle(Toggle::Bold), | 84 | Formatting::Toggle(Toggle::Bold), |
| 85 | Formatting::Toggle(Toggle::Italic), | 85 | Formatting::Toggle(Toggle::Italic), |
| 86 | Formatting::Toggle(Toggle::Underline), | 86 | Formatting::Toggle(Toggle::Underline), |
| ... | @@ -89,17 +89,10 @@ const FORMATS: [Formatting; 8] = [ | ... | @@ -89,17 +89,10 @@ const FORMATS: [Formatting; 8] = [ |
| 89 | Formatting::Numbering, | 89 | Formatting::Numbering, |
| 90 | Formatting::Indent, | 90 | Formatting::Indent, |
| 91 | Formatting::Outdent, | 91 | Formatting::Outdent, |
| 92 | ]; | 92 | Formatting::RemoveTags, |
| 93 | const TAGS: [NoteTag; 9] = [ | 93 | Formatting::Clear, |
| 94 | NoteTag::ToDo, | 94 | // OneNote's first highlighter colour, yellow. |
| 95 | NoteTag::Important, | 95 | Formatting::Highlight(Some(0x0000_ffff)), |
| 96 | NoteTag::Question, | ||
| 97 | NoteTag::RememberForLater, | ||
| 98 | NoteTag::Definition, | ||
| 99 | NoteTag::Highlight, | ||
| 100 | NoteTag::Contact, | ||
| 101 | NoteTag::Address, | ||
| 102 | NoteTag::PhoneNumber, | ||
| 103 | ]; | 96 | ]; |
| 104 | 97 | ||
| 105 | /// The page and its text model, apart from the surface it is drawn on. | 98 | /// The page and its text model, apart from the surface it is drawn on. |
| ... | @@ -217,6 +210,9 @@ impl Canvas { | ... | @@ -217,6 +210,9 @@ impl Canvas { |
| 217 | } | 210 | } |
| 218 | 211 | ||
| 219 | fn target(&self, point: [f32; 2]) -> Target { | 212 | fn target(&self, point: [f32; 2]) -> Target { |
| 213 | if self.page.inserting_space() { | ||
| 214 | return Target::Grip; | ||
| 215 | } | ||
| 220 | match self.page.hit(self.device(point)) { | 216 | match self.page.hit(self.device(point)) { |
| 221 | Some(Hit::Handle { .. } | Hit::Resize { .. }) => Target::Grip, | 217 | Some(Hit::Handle { .. } | Hit::Resize { .. }) => Target::Grip, |
| 222 | Some(Hit::Image { id, .. }) | 218 | Some(Hit::Image { id, .. }) |
| ... | @@ -363,8 +359,8 @@ impl Canvas { | ... | @@ -363,8 +359,8 @@ impl Canvas { |
| 363 | })) | 359 | })) |
| 364 | } | 360 | } |
| 365 | 361 | ||
| 366 | /// The formatting the selection has, as `FORMATS` and `TAGS` bits. | 362 | /// The formatting the selection has, as `FORMATS` and default tag bits. |
| 367 | fn format_bits(&self) -> Result<u32> { | 363 | fn format_bits(&self) -> Result<u64> { |
| 368 | let state = self.page.editor.format_state()?; | 364 | let state = self.page.editor.format_state()?; |
| 369 | let mut bits = 0; | 365 | let mut bits = 0; |
| 370 | for (bit, format) in FORMATS.iter().enumerate() { | 366 | for (bit, format) in FORMATS.iter().enumerate() { |
| ... | @@ -374,17 +370,25 @@ impl Canvas { | ... | @@ -374,17 +370,25 @@ impl Canvas { |
| 374 | Formatting::Numbering => state.numbering, | 370 | Formatting::Numbering => state.numbering, |
| 375 | _ => false, | 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() { | 375 | for (place, tag) in NoteTag::defaults().into_iter().enumerate() { |
| 380 | bits |= u32::from(state.tags.contains(tag)) << (16 + bit); | 376 | let tagged = state.tags.contains(&(tag, place as u16)); |
| 377 | bits |= u64::from(tagged) << (16 + place); | ||
| 381 | } | 378 | } |
| 382 | Ok(bits) | 379 | Ok(bits) |
| 383 | } | 380 | } |
| 384 | 381 | ||
| 385 | fn format(&mut self, command: u8) -> Result<bool> { | 382 | fn format(&mut self, command: u8) -> Result<bool> { |
| 386 | let command = match command { | 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 | _ => FORMATS | 392 | _ => FORMATS |
| 389 | .get(usize::from(command)) | 393 | .get(usize::from(command)) |
| 390 | .ok_or("No such formatting")? | 394 | .ok_or("No such formatting")? |
| ... | @@ -426,6 +430,17 @@ impl Canvas { | ... | @@ -426,6 +430,17 @@ impl Canvas { |
| 426 | Ok(true) | 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 | /// The page's text, outline by outline, as the desktop puts a refused edit's on the | 444 | /// The page's text, outline by outline, as the desktop puts a refused edit's on the |
| 430 | /// clipboard. | 445 | /// clipboard. |
| 431 | fn page_text(&self) -> String { | 446 | fn page_text(&self) -> String { |
| ... | @@ -446,6 +461,27 @@ impl Canvas { | ... | @@ -446,6 +461,27 @@ impl Canvas { |
| 446 | .join("\n\n") | 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 | /// Puts a picture of `size` points after the caret's paragraph. | 485 | /// Puts a picture of `size` points after the caret's paragraph. |
| 450 | fn insert_picture(&mut self, bytes: &[u8], size: [f32; 2]) -> Result<bool> { | 486 | fn insert_picture(&mut self, bytes: &[u8], size: [f32; 2]) -> Result<bool> { |
| 451 | let image = Image { | 487 | let image = Image { |
| ... | @@ -855,15 +891,15 @@ pub extern "C" fn sb_view_can_undo(view: &View, redo: bool) -> bool { | ... | @@ -855,15 +891,15 @@ pub extern "C" fn sb_view_can_undo(view: &View, redo: bool) -> bool { |
| 855 | } | 891 | } |
| 856 | 892 | ||
| 857 | /// The selection's formatting as bits: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4 | 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 | #[unsafe(no_mangle)] | 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 | report(view.canvas.format_bits()).unwrap_or(0) | 897 | report(view.canvas.format_bits()).unwrap_or(0) |
| 862 | } | 898 | } |
| 863 | 899 | ||
| 864 | /// Applies formatting to the selection: 0 bold, 1 italic, 2 underline, 3 strikethrough, 4 | 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 | 901 | /// bullets, 5 numbering, 6 indent, 7 outdent, 8 remove tags, 9 clear formatting, 10 yellow |
| 866 | /// OneNote's default tag `command - 16`, To Do first. | 902 | /// highlight, each toggling where it can; from 16 toggles tag `command - 16` of `sb_tags`. |
| 867 | #[unsafe(no_mangle)] | 903 | #[unsafe(no_mangle)] |
| 868 | pub extern "C" fn sb_view_apply(view: &mut View, command: u8) -> bool { | 904 | pub extern "C" fn sb_view_apply(view: &mut View, command: u8) -> bool { |
| 869 | let result = view.canvas.format(command); | 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,6 +929,156 @@ pub unsafe extern "C" fn sb_view_find(view: &mut View, query: *const c_char) -> |
| 893 | report(view.canvas.find(&string(query))).unwrap_or(false) | 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 | /// The page's text, for the clipboard when an edit is refused; freed with `sb_string_free`. | 1082 | /// The page's text, for the clipboard when an edit is refused; freed with `sb_string_free`. |
| 897 | #[unsafe(no_mangle)] | 1083 | #[unsafe(no_mangle)] |
| 898 | pub extern "C" fn sb_view_page_text(view: &View) -> *mut c_char { | 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,11 +3,11 @@ |
| 3 | //! cache, which stores each edit at once and publishes it in the background. | 3 | //! cache, which stores each edit at once and publishes it in the background. |
| 4 | 4 | ||
| 5 | use crate::{Result, owned, report, string}; | 5 | use crate::{Result, owned, report, string}; |
| 6 | use canvas::search::{Entry, Index, Query}; | 6 | use canvas::search::{Entry, Index, Query, Tagged}; |
| 7 | use notebook::{ | 7 | use notebook::{ |
| 8 | Remote, Replica, | 8 | Remote, Replica, |
| 9 | discover::{Folder, SectionState}, | 9 | discover::{Folder, SectionState}, |
| 10 | session::{self, Event, Notebook}, | 10 | session::{self, Background, Event, Notebook, SyncStatus}, |
| 11 | smb::{Client, Credentials}, | 11 | smb::{Client, Credentials}, |
| 12 | }; | 12 | }; |
| 13 | use onestore::{ | 13 | use onestore::{ |
| ... | @@ -21,8 +21,8 @@ use std::{ | ... | @@ -21,8 +21,8 @@ use std::{ |
| 21 | io, | 21 | io, |
| 22 | path::{Path, PathBuf}, | 22 | path::{Path, PathBuf}, |
| 23 | sync::{ | 23 | sync::{ |
| 24 | Arc, Mutex, OnceLock, | 24 | Arc, Mutex, OnceLock, Weak, |
| 25 | atomic::{AtomicU8, Ordering}, | 25 | atomic::{AtomicBool, AtomicU8, Ordering}, |
| 26 | }, | 26 | }, |
| 27 | time::{Duration, Instant}, | 27 | time::{Duration, Instant}, |
| 28 | }; | 28 | }; |
| ... | @@ -33,6 +33,24 @@ const TIMEOUT: Duration = Duration::from_secs(10); | ... | @@ -33,6 +33,24 @@ const TIMEOUT: Duration = Duration::from_secs(10); |
| 33 | const LIMIT: usize = 256 * 1024 * 1024; | 33 | const LIMIT: usize = 256 * 1024 * 1024; |
| 34 | /// The tab colour OneNote gives a section that stores none, as the desktop shows it. | 34 | /// The tab colour OneNote gives a section that stores none, as the desktop shows it. |
| 35 | const SECTION_COLOR: u32 = 0x00e4_a88a; | 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 | /// Runs `body(context)` within the host's coordinated reading or writing of `path`, as | 55 | /// Runs `body(context)` within the host's coordinated reading or writing of `path`, as |
| 38 | /// `NSFileCoordinator` has other processes (file providers) wait for it, or not at all when | 56 | /// `NSFileCoordinator` has other processes (file providers) wait for it, or not at all when |
| ... | @@ -183,6 +201,12 @@ pub struct Library { | ... | @@ -183,6 +201,12 @@ pub struct Library { |
| 183 | place: Place, | 201 | place: Place, |
| 184 | cache: PathBuf, | 202 | cache: PathBuf, |
| 185 | index: Mutex<(HashMap<String, Checked>, Index)>, | 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 | #[derive(serde::Serialize, serde::Deserialize)] | 212 | #[derive(serde::Serialize, serde::Deserialize)] |
| ... | @@ -244,12 +268,20 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) { | ... | @@ -244,12 +268,20 @@ fn tabs(folder: &Folder, tabs: &mut Vec<Tab>) { |
| 244 | 268 | ||
| 245 | impl Library { | 269 | impl Library { |
| 246 | pub(crate) fn open(path: &Path, cache: &Path) -> Result<Self> { | 270 | pub(crate) fn open(path: &Path, cache: &Path) -> Result<Self> { |
| 247 | let (notebook, place) = if path.is_file() { | 271 | let (notebook, place, background) = if path.is_file() { |
| 248 | (None, Place::File(path.to_owned())) | 272 | (None, Place::File(path.to_owned()), None) |
| 249 | } else { | 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 | Place::Folder(path.to_owned()), | 283 | Place::Folder(path.to_owned()), |
| 284 | Some(background), | ||
| 253 | ) | 285 | ) |
| 254 | }; | 286 | }; |
| 255 | Ok(Self { | 287 | Ok(Self { |
| ... | @@ -257,30 +289,61 @@ impl Library { | ... | @@ -257,30 +289,61 @@ impl Library { |
| 257 | place, | 289 | place, |
| 258 | cache: cache.to_owned(), | 290 | cache: cache.to_owned(), |
| 259 | index: Mutex::default(), | 291 | index: Mutex::default(), |
| 292 | background, | ||
| 293 | open: Mutex::default(), | ||
| 294 | offline: AtomicBool::new(false), | ||
| 260 | }) | 295 | }) |
| 261 | } | 296 | } |
| 262 | 297 | ||
| 263 | /// The notebook folder `root` on `server`; while the server cannot be reached, its sections | 298 | /// The notebook folder `root` on `server`; while the server cannot be reached, its sections |
| 264 | /// as last listed, if it was listed before. | 299 | /// as last listed, if it was listed before. |
| 265 | pub(crate) fn server(server: Server, root: &str, cache: &Path) -> Result<Self> { | 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 | let library = Self { | 310 | let library = Self { |
| 267 | notebook: Mutex::default(), | 311 | notebook: Mutex::default(), |
| 268 | place: Place::Share { | 312 | place: Place::Share { |
| 269 | server: Arc::new(server), | 313 | server, |
| 270 | client: Mutex::default(), | 314 | client: Mutex::default(), |
| 271 | root: root.to_owned(), | 315 | root: root.to_owned(), |
| 272 | }, | 316 | }, |
| 273 | cache: cache.to_owned(), | 317 | cache: cache.to_owned(), |
| 274 | index: Mutex::default(), | 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(())) | 323 | let reached = library.with_notebook(false, |_| Ok(())); |
| 277 | && library.listed().is_none() | 324 | match library.listed() { |
| 278 | { | 325 | Some(listed) => library.watch(&listed.files), |
| 279 | return Err(error); | 326 | None => reached?, |
| 280 | } | 327 | } |
| 281 | Ok(library) | 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 | /// Where a share notebook's last listing is kept. | 347 | /// Where a share notebook's last listing is kept. |
| 285 | fn listing(&self) -> Option<PathBuf> { | 348 | fn listing(&self) -> Option<PathBuf> { |
| 286 | let Place::Share { server, root, .. } = &self.place else { | 349 | let Place::Share { server, root, .. } = &self.place else { |
| ... | @@ -366,6 +429,9 @@ impl Library { | ... | @@ -366,6 +429,9 @@ impl Library { |
| 366 | }; | 429 | }; |
| 367 | tabs(notebook.catalog(), &mut listed.tabs); | 430 | tabs(notebook.catalog(), &mut listed.tabs); |
| 368 | files(notebook.catalog(), &mut listed.files); | 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 | Ok(listed) | 435 | Ok(listed) |
| 370 | }); | 436 | }); |
| 371 | let Some(listing) = self.listing() else { | 437 | let Some(listing) = self.listing() else { |
| ... | @@ -375,14 +441,42 @@ impl Library { | ... | @@ -375,14 +441,42 @@ impl Library { |
| 375 | Ok(listed) => { | 441 | Ok(listed) => { |
| 376 | std::fs::create_dir_all(listing.parent().ok_or("No cache")?)?; | 442 | std::fs::create_dir_all(listing.parent().ok_or("No cache")?)?; |
| 377 | std::fs::write(&listing, serde_json::to_vec(&listed)?)?; | 443 | std::fs::write(&listing, serde_json::to_vec(&listed)?)?; |
| 444 | self.watch(&listed.files); | ||
| 378 | Ok(listed.tabs) | 445 | Ok(listed.tabs) |
| 379 | } | 446 | } |
| 380 | Err(error) => Ok(self.listed().ok_or(error)?.tabs), | 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 | pub(crate) fn section( | 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 | &self, | 480 | &self, |
| 387 | path: &str, | 481 | path: &str, |
| 388 | notify: impl Fn() + Send + 'static, | 482 | notify: impl Fn() + Send + 'static, |
| ... | @@ -518,6 +612,27 @@ fn dated(author: &str, date: &str, time: &str) -> Result<PageCreation> { | ... | @@ -518,6 +612,27 @@ fn dated(author: &str, date: &str, time: &str) -> Result<PageCreation> { |
| 518 | } | 612 | } |
| 519 | 613 | ||
| 520 | impl Section { | 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 | pub(crate) fn rows(&self) -> Result<Vec<Row>> { | 636 | pub(crate) fn rows(&self) -> Result<Vec<Row>> { |
| 522 | let section = &self.shared.section; | 637 | let section = &self.shared.section; |
| 523 | let conflicts = section.conflicts()?; | 638 | let conflicts = section.conflicts()?; |
| ... | @@ -684,9 +799,50 @@ pub(crate) struct Found { | ... | @@ -684,9 +799,50 @@ pub(crate) struct Found { |
| 684 | 799 | ||
| 685 | impl Library { | 800 | impl Library { |
| 686 | /// Pages of the notebook holding every word of `query` in their title or text, as the | 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 | 802 | /// desktop searches. |
| 688 | /// again when their file changed. | ||
| 689 | pub(crate) fn search(&self, open: Option<(&str, &Section)>, query: &str) -> Result<Vec<Found>> { | 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 | let tabs: Vec<String> = self | 846 | let tabs: Vec<String> = self |
| 691 | .tabs()? | 847 | .tabs()? |
| 692 | .into_iter() | 848 | .into_iter() |
| ... | @@ -714,18 +870,73 @@ impl Library { | ... | @@ -714,18 +870,73 @@ impl Library { |
| 714 | } | 870 | } |
| 715 | } | 871 | } |
| 716 | index.retain(|entry| tabs.contains(&entry.section)); | 872 | index.retain(|entry| tabs.contains(&entry.section)); |
| 717 | Ok(index | 873 | Ok(read(index)) |
| 718 | .search(&Query::new(query), |_| true) | 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 | .into_iter() | 894 | .into_iter() |
| 720 | .map(|found| Found { | 895 | .map(|(path, status): (String, SyncStatus)| Synced { |
| 721 | section: found.section, | 896 | state: status.state() as u8, |
| 722 | page: found.space.to_string(), | 897 | synced: status.synced.map(unix), |
| 723 | title: found.title.replace(|char: char| char.is_control(), " "), | 898 | queued: status.queued, |
| 724 | snippet: found.snippet, | 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 | /// Reads the section at catalog `path` into `index` again when its file changed, and | 940 | /// Reads the section at catalog `path` into `index` again when its file changed, and |
| 730 | /// checks at most every 30 seconds. | 941 | /// checks at most every 30 seconds. |
| 731 | fn reindex( | 942 | fn reindex( |
| ... | @@ -814,12 +1025,12 @@ fn boxed<T>(result: Result<T>, error: *mut *mut c_char) -> *mut T { | ... | @@ -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 | report(value.and_then(|value| Ok(serde_json::to_string(&value)?))) | 1029 | report(value.and_then(|value| Ok(serde_json::to_string(&value)?))) |
| 819 | .map_or(std::ptr::null_mut(), owned) | 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 | (!text.is_null()).then(|| string(text)) | 1034 | (!text.is_null()).then(|| string(text)) |
| 824 | } | 1035 | } |
| 825 | 1036 | ||
| ... | @@ -925,6 +1136,55 @@ pub unsafe extern "C" fn sb_library_search( | ... | @@ -925,6 +1136,55 @@ pub unsafe extern "C" fn sb_library_search( |
| 925 | json(library.search(open.map(|section| (path.as_str(), section)), &string(query))) | 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 | /// # Safety | 1188 | /// # Safety |
| 929 | /// `library` came from `sb_library_open` or `sb_library_server` and is not used again. | 1189 | /// `library` came from `sb_library_open` or `sb_library_server` and is not used again. |
| 930 | #[unsafe(no_mangle)] | 1190 | #[unsafe(no_mangle)] |
| ... | @@ -1050,16 +1310,7 @@ pub unsafe extern "C" fn sb_section_open( | ... | @@ -1050,16 +1310,7 @@ pub unsafe extern "C" fn sb_section_open( |
| 1050 | Arc::from_raw(library as *const Library) | 1310 | Arc::from_raw(library as *const Library) |
| 1051 | }; | 1311 | }; |
| 1052 | boxed( | 1312 | boxed( |
| 1053 | library | 1313 | Section::open(library, &string(path), string(author), move || wake(token)), |
| 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 | }), | ||
| 1063 | error, | 1314 | error, |
| 1064 | ) | 1315 | ) |
| 1065 | } | 1316 | } |
crates/mobile/src/tests.rs+206-10| ... | @@ -1,4 +1,6 @@ | ... | @@ -1,4 +1,6 @@ |
| 1 | use super::*; | 1 | use super::*; |
| 2 | use library::{sb_library_set_offline, sb_library_sync_now}; | ||
| 3 | use notebook::session::SyncState; | ||
| 2 | use std::path::{Path, PathBuf}; | 4 | use std::path::{Path, PathBuf}; |
| 3 | 5 | ||
| 4 | const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus"); | 6 | const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus"); |
| ... | @@ -24,15 +26,7 @@ fn copy(path: &str) -> (tempfile::TempDir, PathBuf) { | ... | @@ -24,15 +26,7 @@ fn copy(path: &str) -> (tempfile::TempDir, PathBuf) { |
| 24 | } | 26 | } |
| 25 | 27 | ||
| 26 | fn open(library: &Arc<Library>, path: &str) -> Section { | 28 | fn open(library: &Arc<Library>, path: &str) -> Section { |
| 27 | let section = library.section(path, || {}).unwrap(); | 29 | Section::open(Arc::clone(library), path, "Clover Test".into(), || {}).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 | } | ||
| 36 | } | 30 | } |
| 37 | 31 | ||
| 38 | fn space(section: &Section, title: &str) -> ExGuid { | 32 | fn space(section: &Section, title: &str) -> ExGuid { |
| ... | @@ -230,7 +224,20 @@ fn undo_restores_typing() { | ... | @@ -230,7 +224,20 @@ fn undo_restores_typing() { |
| 230 | let mut canvas = canvas(&section, "Paragraph controls"); | 224 | let mut canvas = canvas(&section, "Paragraph controls"); |
| 231 | focus(&mut canvas, "Collapsed parent"); | 225 | focus(&mut canvas, "Collapsed parent"); |
| 232 | let before = canvas.active().shown_text(); | 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 | assert_ne!(canvas.active().shown_text(), before); | 241 | assert_ne!(canvas.active().shown_text(), before); |
| 235 | assert!(moved(canvas.page.undo(false).unwrap())); | 242 | assert!(moved(canvas.page.undo(false).unwrap())); |
| 236 | assert_eq!(canvas.active().shown_text(), before); | 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,3 +636,192 @@ fn tapping_the_date_asks_for_it_and_a_new_date_is_stored() { |
| 629 | assert_eq!(stored.unwrap()[0], "Friday, January 2, 2026"); | 636 | assert_eq!(stored.unwrap()[0], "Friday, January 2, 2026"); |
| 630 | assert_ne!(canvas.page.editor.date().unwrap().timestamp(), before); | 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,7 +14,7 @@ use rusqlite::{Connection, OpenFlags, TransactionBehavior, params}; |
| 14 | use std::{ | 14 | use std::{ |
| 15 | fs::OpenOptions, | 15 | fs::OpenOptions, |
| 16 | io, | 16 | io, |
| 17 | path::Path, | 17 | path::{Path, PathBuf}, |
| 18 | sync::{Arc, Mutex, MutexGuard, mpsc}, | 18 | sync::{Arc, Mutex, MutexGuard, mpsc}, |
| 19 | time::Duration, | 19 | time::Duration, |
| 20 | }; | 20 | }; |
| ... | @@ -106,6 +106,25 @@ impl Replica { | ... | @@ -106,6 +106,25 @@ impl Replica { |
| 106 | /// An initialization error preserves the created file for inspection. | 106 | /// An initialization error preserves the created file for inspection. |
| 107 | pub fn create(path: impl AsRef<Path>, source: &[u8]) -> Result<Self> { | 107 | pub fn create(path: impl AsRef<Path>, source: &[u8]) -> Result<Self> { |
| 108 | validate(source)?; | 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 | let mut options = OpenOptions::new(); | 128 | let mut options = OpenOptions::new(); |
| 110 | options.read(true).write(true).create_new(true); | 129 | options.read(true).write(true).create_new(true); |
| 111 | #[cfg(unix)] | 130 | #[cfg(unix)] |
| ... | @@ -113,8 +132,8 @@ impl Replica { | ... | @@ -113,8 +132,8 @@ impl Replica { |
| 113 | use std::os::unix::fs::OpenOptionsExt; | 132 | use std::os::unix::fs::OpenOptionsExt; |
| 114 | options.mode(0o600); | 133 | options.mode(0o600); |
| 115 | } | 134 | } |
| 116 | drop(options.open(path.as_ref())?); | 135 | drop(options.open(path)?); |
| 117 | let mut connection = cache_connection(path.as_ref())?; | 136 | let mut connection = cache_connection(path)?; |
| 118 | write_ahead(&connection)?; | 137 | write_ahead(&connection)?; |
| 119 | let transaction = connection.transaction_with_behavior(TransactionBehavior::Exclusive)?; | 138 | let transaction = connection.transaction_with_behavior(TransactionBehavior::Exclusive)?; |
| 120 | let tables: i64 = | 139 | let tables: i64 = |
| ... | @@ -133,7 +152,7 @@ impl Replica { | ... | @@ -133,7 +152,7 @@ impl Replica { |
| 133 | schema::create(&transaction)?; | 152 | schema::create(&transaction)?; |
| 134 | base::write(&transaction, base::Image::Base, source)?; | 153 | base::write(&transaction, base::Image::Base, source)?; |
| 135 | transaction.commit()?; | 154 | transaction.commit()?; |
| 136 | Self::start(connection) | 155 | Ok(connection.close().map_err(|(_, error)| error)?) |
| 137 | } | 156 | } |
| 138 | 157 | ||
| 139 | /// Reopens an existing cache and its durable pending edits without network access. | 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,7 +696,9 @@ impl Diff { |
| 696 | let part = |index: &Index| { | 696 | let part = |index: &Index| { |
| 697 | index.nodes.get(object).map(|node| match (&node.own, edit) { | 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 | OutlineEdit::Position { .. }, | 702 | OutlineEdit::Position { .. }, |
| 701 | ) => (layout.x, layout.y, None, None), | 703 | ) => (layout.x, layout.y, None, None), |
| 702 | (Own::Outline { layout, .. }, OutlineEdit::Width { .. }) => ( | 704 | (Own::Outline { layout, .. }, OutlineEdit::Width { .. }) => ( |
| ... | @@ -1073,6 +1075,9 @@ impl Index { | ... | @@ -1073,6 +1075,9 @@ impl Index { |
| 1073 | PageObject::Image(image) => { | 1075 | PageObject::Image(image) => { |
| 1074 | index.add(image.id, PAGE, Own::Image(image.clone()), Vec::new()) | 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 | PageObject::Ink(ink) => index.add(ink.id, PAGE, Own::Ink(ink.clone()), Vec::new()), | 1081 | PageObject::Ink(ink) => index.add(ink.id, PAGE, Own::Ink(ink.clone()), Vec::new()), |
| 1077 | PageObject::Unsupported(object) => index.add( | 1082 | PageObject::Unsupported(object) => index.add( |
| 1078 | object.id, | 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,6 +47,10 @@ fn object(object: &mut PageObject, visit: &mut impl FnMut(&mut Payload)) { |
| 47 | } | 47 | } |
| 48 | } | 48 | } |
| 49 | PageObject::Image(image) => visit(&mut image.bytes), | 49 | PageObject::Image(image) => visit(&mut image.bytes), |
| 50 | PageObject::Attachment(attachment) => { | ||
| 51 | visit(&mut attachment.bytes); | ||
| 52 | visit(&mut attachment.preview); | ||
| 53 | } | ||
| 50 | PageObject::Ink(_) | PageObject::Unsupported(_) => {} | 54 | PageObject::Ink(_) | PageObject::Unsupported(_) => {} |
| 51 | } | 55 | } |
| 52 | } | 56 | } |
crates/notebook/src/session.rs+46-2| ... | @@ -919,6 +919,17 @@ impl Notebook { | ... | @@ -919,6 +919,17 @@ impl Notebook { |
| 919 | &self, | 919 | &self, |
| 920 | interval: Duration, | 920 | interval: Duration, |
| 921 | notify: impl Fn() + Send + 'static, | 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 | ) -> Result<Background> { | 933 | ) -> Result<Background> { |
| 923 | let Some(root) = self.root.clone() else { | 934 | let Some(root) = self.root.clone() else { |
| 924 | return Err(io::Error::new( | 935 | return Err(io::Error::new( |
| ... | @@ -930,8 +941,8 @@ impl Notebook { | ... | @@ -930,8 +941,8 @@ impl Notebook { |
| 930 | Background::start( | 941 | Background::start( |
| 931 | interval, | 942 | interval, |
| 932 | move || { | 943 | move || { |
| 933 | let root = root.clone(); | 944 | let (root, remote) = (root.clone(), remote.clone()); |
| 934 | Ok(move |path: &str| FileRemote(root.join(path))) | 945 | Ok(move |path: &str| remote(&root.join(path))) |
| 935 | }, | 946 | }, |
| 936 | notify, | 947 | notify, |
| 937 | ) | 948 | ) |
| ... | @@ -1110,6 +1121,39 @@ pub struct SyncStatus { | ... | @@ -1110,6 +1121,39 @@ pub struct SyncStatus { |
| 1110 | pub queued: u64, | 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 | /// The last attempt's outcome; `None` before the first. | 1157 | /// The last attempt's outcome; `None` before the first. |
| 1114 | type Observed = Option<Option<io::Error>>; | 1158 | type Observed = Option<Option<io::Error>>; |
| 1115 | 1159 |
crates/notebook/tests/copy.rs+4-3| ... | @@ -11,11 +11,12 @@ use std::{ | ... | @@ -11,11 +11,12 @@ use std::{ |
| 11 | time::{Duration, Instant}, | 11 | time::{Duration, Instant}, |
| 12 | }; | 12 | }; |
| 13 | 13 | ||
| 14 | /// Pages the writer authored for other rows: text with an attachment and its icon, a | 14 | /// Pages the writer authored for other rows: text with an attachment and its icon, files on |
| 15 | /// table, nested tables with cell subtrees, an inserted picture, a page-level ink drawing, tags, lists, equations | 15 | /// the page, a table, nested tables with cell subtrees, an inserted picture, a page-level ink |
| 16 | /// and paragraph formatting. | 16 | /// drawing, tags, lists, equations and paragraph formatting. |
| 17 | const SOURCES: &[&str] = &[ | 17 | const SOURCES: &[&str] = &[ |
| 18 | "attachment-edit/icon/candidate/files.one", | 18 | "attachment-edit/icon/candidate/files.one", |
| 19 | "attachment-floating/candidate/files.one", | ||
| 19 | "table-edit/created/candidate/tables.one", | 20 | "table-edit/created/candidate/tables.one", |
| 20 | "table-edit/nested/candidate/synthetic.one", | 21 | "table-edit/nested/candidate/synthetic.one", |
| 21 | "picture-edit/inserted/candidate/pictures.one", | 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,3 +614,93 @@ fn a_new_native_wrap_reservation_keeps_the_local_width_on_a_conflict_page() { |
| 614 | assert_eq!(layout.max_width, Some(144.0)); | 614 | assert_eq!(layout.max_width, Some(144.0)); |
| 615 | assert!(layout.reserved_width.is_none()); | 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,7 +46,7 @@ harness also accepts `--client-profile release`. |
| 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 | | 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 | | `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 | | 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 | | `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 | | 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 | | `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned | | 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 | | `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode | | 51 | | `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode | |
| 52 | | `PageCreation`, `SectionOp::Create` | Add an empty top-level page and its section entry atomically, under identities the intent retains across retries | | 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,7 +56,7 @@ harness also accepts `--client-profile release`. |
| 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 | | 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 | | `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 | | 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 | | `TextAttribute`, `PageOp::Format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | 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 | | `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 | | 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 | | `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 | | 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 | | `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 | | 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,25 +1090,7 @@ impl<'a> Writer<'_, 'a> { |
| 1090 | self.picture(image, Some(paragraph.id)) | 1090 | self.picture(image, Some(paragraph.id)) |
| 1091 | } | 1091 | } |
| 1092 | ParagraphContent::Attachment(attachment) => { | 1092 | ParagraphContent::Attachment(attachment) => { |
| 1093 | let Some(bytes) = &attachment.bytes else { | 1093 | self.attachment(attachment, Some(paragraph.id)) |
| 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 | }) | ||
| 1112 | } | 1094 | } |
| 1113 | ParagraphContent::Ink(ink) => self.ink(ink, Some(paragraph.id)), | 1095 | ParagraphContent::Ink(ink) => self.ink(ink, Some(paragraph.id)), |
| 1114 | ParagraphContent::Unsupported(_) => { | 1096 | ParagraphContent::Unsupported(_) => { |
| ... | @@ -1136,6 +1118,32 @@ impl<'a> Writer<'_, 'a> { | ... | @@ -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 | fn ink(&mut self, ink: &Ink, holder: Option<ExGuid>) -> Result<(), Failure> { | 1147 | fn ink(&mut self, ink: &Ink, holder: Option<ExGuid>) -> Result<(), Failure> { |
| 1140 | for stroke in &ink.strokes { | 1148 | for stroke in &ink.strokes { |
| 1141 | self.free(stroke.id)?; | 1149 | self.free(stroke.id)?; |
| ... | @@ -1166,13 +1174,14 @@ impl<'a> Writer<'_, 'a> { | ... | @@ -1166,13 +1174,14 @@ impl<'a> Writer<'_, 'a> { |
| 1166 | self.free(id)?; | 1174 | self.free(id)?; |
| 1167 | } | 1175 | } |
| 1168 | } | 1176 | } |
| 1169 | if first.parent.is_some() || first.level != 1 { | 1177 | if first.parent.is_some() { |
| 1170 | return Err(OpError::Unsupported("An outline starts at its first level").into()); | 1178 | return Err(OpError::Unsupported("An outline's first paragraph has no parent").into()); |
| 1171 | } | 1179 | } |
| 1172 | self.paragraph(outline.id, None, first, Some((outline.id, x, y)))?; | 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. | 1181 | // Later paragraphs follow the first, their levels as the outline gives them; a |
| 1174 | if !rest.is_empty() { | 1182 | // deeper first paragraph goes in a group, as OneNote stores a tabbed one. |
| 1175 | let mut targets = vec![(first.id, 1)]; | 1183 | if !rest.is_empty() || first.level != 1 { |
| 1184 | let mut targets = vec![(first.id, first.level)]; | ||
| 1176 | for paragraph in rest { | 1185 | for paragraph in rest { |
| 1177 | let parent = paragraph.parent.unwrap_or(outline.id); | 1186 | let parent = paragraph.parent.unwrap_or(outline.id); |
| 1178 | let parent_level = rest | 1187 | let parent_level = rest |
| ... | @@ -1226,6 +1235,7 @@ impl<'a> Writer<'_, 'a> { | ... | @@ -1226,6 +1235,7 @@ impl<'a> Writer<'_, 'a> { |
| 1226 | } | 1235 | } |
| 1227 | self.picture(image, None)?; | 1236 | self.picture(image, None)?; |
| 1228 | } | 1237 | } |
| 1238 | PageObject::Attachment(attachment) => self.attachment(attachment, None)?, | ||
| 1229 | PageObject::Ink(ink) => self.ink(ink, None)?, | 1239 | PageObject::Ink(ink) => self.ink(ink, None)?, |
| 1230 | PageObject::Title(_) | PageObject::Unsupported(_) => { | 1240 | PageObject::Title(_) | PageObject::Unsupported(_) => { |
| 1231 | return Err( | 1241 | return Err( |
crates/onestore/src/op/content.rs+23-6| ... | @@ -218,7 +218,9 @@ pub(crate) fn picture_changes( | ... | @@ -218,7 +218,9 @@ pub(crate) fn picture_changes( |
| 218 | [0xff, 0xd8, 0xff, ..] => ".jpg", | 218 | [0xff, 0xd8, 0xff, ..] => ".jpg", |
| 219 | [b'G', b'I', b'F', b'8', ..] => ".gif", | 219 | [b'G', b'I', b'F', b'8', ..] => ".gif", |
| 220 | [b'B', b'M', ..] => ".bmp", | 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 | let modified = crate::create::current_timestamps()?.0.to_le_bytes(); | 225 | let modified = crate::create::current_timestamps()?.0.to_le_bytes(); |
| 224 | let mut values: Values = vec![(0x14001d7a, modified.to_vec())]; | 226 | let mut values: Values = vec![(0x14001d7a, modified.to_vec())]; |
| ... | @@ -303,12 +305,13 @@ pub(crate) struct AttachmentIds { | ... | @@ -303,12 +305,13 @@ pub(crate) struct AttachmentIds { |
| 303 | 305 | ||
| 304 | /// What OneNote stores for an inserted file: an embedded-file container declaring the | 306 | /// What OneNote stores for an inserted file: an embedded-file container declaring the |
| 305 | /// payload and an attachment object naming the file, which paragraph `holder` holds as | 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 | pub(crate) fn attachment_changes( | 310 | pub(crate) fn attachment_changes( |
| 308 | active: &ActivePage<'_>, | 311 | active: &ActivePage<'_>, |
| 309 | attachment: &Attachment, | 312 | attachment: &Attachment, |
| 310 | ids: &AttachmentIds, | 313 | ids: &AttachmentIds, |
| 311 | holder: ExGuid, | 314 | holder: Option<ExGuid>, |
| 312 | ) -> Result<Changes, Error> { | 315 | ) -> Result<Changes, Error> { |
| 313 | let name = attachment.filename.as_str(); | 316 | let name = attachment.filename.as_str(); |
| 314 | if name.is_empty() || name.contains(['\0', '/', '\\']) { | 317 | if name.is_empty() || name.contains(['\0', '/', '\\']) { |
| ... | @@ -351,13 +354,27 @@ pub(crate) fn attachment_changes( | ... | @@ -351,13 +354,27 @@ pub(crate) fn attachment_changes( |
| 351 | .flat_map(|v| v.to_le_bytes()) | 354 | .flat_map(|v| v.to_le_bytes()) |
| 352 | .collect(), | 355 | .collect(), |
| 353 | )); | 356 | )); |
| 354 | values.push((0x14001c3e, 1u32.to_le_bytes().to_vec())); | 357 | let positioned = attachment.layout.x.is_some() || attachment.layout.y.is_some(); |
| 355 | values.push((0x14001c84, 1u32.to_le_bytes().to_vec())); | 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 | values.push((0x1c001c22, crate::create::string(name))); | 372 | values.push((0x1c001c22, crate::create::string(name))); |
| 357 | values.push((0x1c001d9c, crate::create::string(name))); | 373 | values.push((0x1c001d9c, crate::create::string(name))); |
| 358 | if let Some(path) = &attachment.source_path { | 374 | if let Some(path) = &attachment.source_path { |
| 359 | values.push((0x1c001d9d, crate::create::string(path))); | 375 | values.push((0x1c001d9d, crate::create::string(path))); |
| 360 | } | 376 | } |
| 377 | values.extend(layout_values(&Default::default(), &attachment.layout)?.0); | ||
| 361 | let mut changed = BTreeMap::new(); | 378 | let mut changed = BTreeMap::new(); |
| 362 | let mut file = PropertyObject::file(ids.file, &payload_reference(ids.payload), extension)?; | 379 | let mut file = PropertyObject::file(ids.file, &payload_reference(ids.payload), extension)?; |
| 363 | file.jcid = crate::write::EMBEDDED_FILE_JCID; | 380 | file.jcid = crate::write::EMBEDDED_FILE_JCID; |
| ... | @@ -379,7 +396,7 @@ pub(crate) fn attachment_changes( | ... | @@ -379,7 +396,7 @@ pub(crate) fn attachment_changes( |
| 379 | node.set(&[(0x20001c3f, &reference)])?; | 396 | node.set(&[(0x20001c3f, &reference)])?; |
| 380 | } | 397 | } |
| 381 | changed.insert(ids.object, node); | 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 | Ok(changed) | 400 | Ok(changed) |
| 384 | } | 401 | } |
| 385 | 402 |
crates/onestore/src/op/lower.rs+79-9| ... | @@ -103,7 +103,10 @@ impl<'a> View<'a> { | ... | @@ -103,7 +103,10 @@ impl<'a> View<'a> { |
| 103 | view.outline(outline)?; | 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 | Ok(view) | 112 | Ok(view) |
| ... | @@ -191,9 +194,10 @@ impl<'a> View<'a> { | ... | @@ -191,9 +194,10 @@ impl<'a> View<'a> { |
| 191 | let outlines: Vec<&Outline> = match object { | 194 | let outlines: Vec<&Outline> = match object { |
| 192 | PageObject::Outline(outline) => vec![outline], | 195 | PageObject::Outline(outline) => vec![outline], |
| 193 | PageObject::Title(title) => title.outlines.iter().collect(), | 196 | PageObject::Title(title) => title.outlines.iter().collect(), |
| 194 | PageObject::Image(_) | PageObject::Ink(_) | PageObject::Unsupported(_) => { | 197 | PageObject::Image(_) |
| 195 | Vec::new() | 198 | | PageObject::Attachment(_) |
| 196 | } | 199 | | PageObject::Ink(_) |
| 200 | | PageObject::Unsupported(_) => Vec::new(), | ||
| 197 | }; | 201 | }; |
| 198 | for outline in outlines { | 202 | for outline in outlines { |
| 199 | containers.push(outline.id); | 203 | containers.push(outline.id); |
| ... | @@ -262,7 +266,10 @@ pub(crate) fn validate( | ... | @@ -262,7 +266,10 @@ pub(crate) fn validate( |
| 262 | title.layout, | 266 | title.layout, |
| 263 | title.outlines.iter().map(|o| o.id).collect::<Vec<_>>() | 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 | .collect(); | 274 | .collect(); |
| 268 | fixed.sort(); | 275 | fixed.sort(); |
| ... | @@ -667,7 +674,18 @@ pub(crate) fn paragraph_fields( | ... | @@ -667,7 +674,18 @@ pub(crate) fn paragraph_fields( |
| 667 | space_before: field!(space_before), | 674 | space_before: field!(space_before), |
| 668 | space_after: field!(space_after), | 675 | space_after: field!(space_after), |
| 669 | line_spacing: field!(line_spacing), | 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,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 | let inks = |page: &Page| -> BTreeMap<ExGuid, crate::page::Ink> { | 1055 | let inks = |page: &Page| -> BTreeMap<ExGuid, crate::page::Ink> { |
| 990 | page.objects | 1056 | page.objects |
| 991 | .iter() | 1057 | .iter() |
| ... | @@ -1051,7 +1117,9 @@ impl Lowering { | ... | @@ -1051,7 +1117,9 @@ impl Lowering { |
| 1051 | ..(*outline).clone() | 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 | PageObject::Title(_) | PageObject::Unsupported(_) => { | 1123 | PageObject::Title(_) | PageObject::Unsupported(_) => { |
| 1056 | return Err(invalid( | 1124 | return Err(invalid( |
| 1057 | "Titles and unsupported objects cannot be edited through the page model", | 1125 | "Titles and unsupported objects cannot be edited through the page model", |
| ... | @@ -1513,8 +1581,10 @@ impl Lowering { | ... | @@ -1513,8 +1581,10 @@ impl Lowering { |
| 1513 | } | 1581 | } |
| 1514 | let kept: BTreeSet<ExGuid> = new.page.objects.iter().map(PageObject::id).collect(); | 1582 | let kept: BTreeSet<ExGuid> = new.page.objects.iter().map(PageObject::id).collect(); |
| 1515 | for object in &old.page.objects { | 1583 | for object in &old.page.objects { |
| 1516 | if matches!(object, PageObject::Image(_) | PageObject::Ink(_)) | 1584 | if matches!( |
| 1517 | && !kept.contains(&object.id()) | 1585 | object, |
| 1586 | PageObject::Image(_) | PageObject::Attachment(_) | PageObject::Ink(_) | ||
| 1587 | ) && !kept.contains(&object.id()) | ||
| 1518 | { | 1588 | { |
| 1519 | self.emit(PageOp::Delete { | 1589 | self.emit(PageOp::Delete { |
| 1520 | object: object.id(), | 1590 | object: object.id(), |
crates/onestore/src/op/mod.rs+3-3| ... | @@ -114,7 +114,7 @@ pub enum PageOp { | ... | @@ -114,7 +114,7 @@ pub enum PageOp { |
| 114 | right: ExGuid, | 114 | right: ExGuid, |
| 115 | }, | 115 | }, |
| 116 | /// Moves a subtree before a direct child of `parent`, or last; `None` names the page, | 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 | /// lies deeper than its new parent. | 118 | /// lies deeper than its new parent. |
| 119 | Move { | 119 | Move { |
| 120 | object: ExGuid, | 120 | object: ExGuid, |
| ... | @@ -163,8 +163,8 @@ pub enum PageOp { | ... | @@ -163,8 +163,8 @@ pub enum PageOp { |
| 163 | tags: Vec<Tag>, | 163 | tags: Vec<Tag>, |
| 164 | definitions: Vec<(ExGuid, Definition)>, | 164 | definitions: Vec<(ExGuid, Definition)>, |
| 165 | }, | 165 | }, |
| 166 | /// Adds an outline with its paragraphs, a picture with its payload, or ink, before a | 166 | /// Adds an outline with its paragraphs, a picture or file with its payloads, or ink, |
| 167 | /// page child or on top. | 167 | /// before a page child or on top. |
| 168 | Add { | 168 | Add { |
| 169 | object: PageObject, | 169 | object: PageObject, |
| 170 | before: Option<ExGuid>, | 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,7 +194,9 @@ pub(crate) fn holds(page: &Page, id: ExGuid) -> bool { |
| 194 | .iter() | 194 | .iter() |
| 195 | .any(|o| o.id == id || identities(&o.paragraphs).contains(&id)), | 195 | .any(|o| o.id == id || identities(&o.paragraphs).contains(&id)), |
| 196 | PageObject::Ink(ink) => ink.strokes.iter().any(|s| s.id == id), | 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,6 +928,7 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 926 | OutlineEdit::Position { x, y } => { | 928 | OutlineEdit::Position { x, y } => { |
| 927 | let layout = match page.objects.iter_mut().find(|o| o.id() == *object) { | 929 | let layout = match page.objects.iter_mut().find(|o| o.id() == *object) { |
| 928 | Some(PageObject::Ink(ink)) => &mut ink.layout, | 930 | Some(PageObject::Ink(ink)) => &mut ink.layout, |
| 931 | Some(PageObject::Attachment(file)) => &mut file.layout, | ||
| 929 | _ => &mut outline_mut(page, *object)?.layout, | 932 | _ => &mut outline_mut(page, *object)?.layout, |
| 930 | }; | 933 | }; |
| 931 | layout.x = Some(stored(*x)); | 934 | layout.x = Some(stored(*x)); |
| ... | @@ -1090,12 +1093,29 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { | ... | @@ -1090,12 +1093,29 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 1090 | PageObject::Outline(outline) | 1093 | PageObject::Outline(outline) |
| 1091 | } | 1094 | } |
| 1092 | PageObject::Ink(ink) => PageObject::Ink(inked(ink)), | 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 | object => object.clone(), | 1112 | object => object.clone(), |
| 1094 | }; | 1113 | }; |
| 1095 | let outline = matches!(object, PageObject::Outline(_)); | 1114 | let outline = matches!(object, PageObject::Outline(_)); |
| 1096 | let at = page_position(page, *before)?; | 1115 | let at = page_position(page, *before)?; |
| 1097 | page.objects.insert(at, object); | 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 | retitle(page, None); | 1119 | retitle(page, None); |
| 1100 | } | 1120 | } |
| 1101 | } | 1121 | } |
| ... | @@ -1123,19 +1143,7 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { | ... | @@ -1123,19 +1143,7 @@ fn interpret(page: &mut Page, op: &PageOp) -> Result<(), Error> { |
| 1123 | source_path, | 1143 | source_path, |
| 1124 | size, | 1144 | size, |
| 1125 | } => { | 1145 | } => { |
| 1126 | let (path, index) = lists(page) | 1146 | let stored_attachment = attachment_mut(page, *attachment)?; |
| 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 | }; | ||
| 1139 | stored_attachment.filename = filename.clone(); | 1147 | stored_attachment.filename = filename.clone(); |
| 1140 | stored_attachment.source_path = source_path.clone(); | 1148 | stored_attachment.source_path = source_path.clone(); |
| 1141 | stored_attachment.size = size.map(|[w, h]| [stored(w), stored(h)]); | 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,6 +1313,27 @@ fn image_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Image, Err |
| 1305 | Ok(image) | 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 | fn ink_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Ink, Error> { | 1337 | fn ink_mut(page: &mut Page, id: ExGuid) -> Result<&mut crate::page::Ink, Error> { |
| 1309 | if let Some(at) = page.objects.iter().position(|o| o.id() == id) { | 1338 | if let Some(at) = page.objects.iter().position(|o| o.id() == id) { |
| 1310 | let PageObject::Ink(ink) = &mut page.objects[at] else { | 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,11 +141,17 @@ fn tag_definition_values(definition: &Definition) -> Result<Values, Error> { |
| 141 | else { | 141 | else { |
| 142 | return Err(invalid("A note tag must reference a tag definition")); | 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 | let mut values: Values = vec![ | 150 | let mut values: Values = vec![ |
| 145 | (0x0c003473, vec![0]), | 151 | (0x0c003473, vec![0]), |
| 146 | (0x10003463, action_type.unwrap_or(0).to_le_bytes().to_vec()), | 152 | (0x10003463, action_type.unwrap_or(0).to_le_bytes().to_vec()), |
| 147 | (0x10003464, shape.unwrap_or(0).to_le_bytes().to_vec()), | 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 | if let Some(label) = label { | 156 | if let Some(label) = label { |
| 151 | values.push((0x1c003468, crate::create::string(label))); | 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,6 +1105,7 @@ pub(super) fn mutate(page: &mut Page, rng: &mut Rng, family: Family) -> bool { |
| 1105 | filename: "notes.txt".into(), | 1105 | filename: "notes.txt".into(), |
| 1106 | source_path: Some(r"C:\notes.txt".into()), | 1106 | source_path: Some(r"C:\notes.txt".into()), |
| 1107 | size: None, | 1107 | size: None, |
| 1108 | layout: Layout::default(), | ||
| 1108 | bytes: Some(b"attached bytes".as_slice().into()), | 1109 | bytes: Some(b"attached bytes".as_slice().into()), |
| 1109 | preview: None, | 1110 | preview: None, |
| 1110 | recording: None, | 1111 | recording: None, |
| ... | @@ -2957,3 +2958,20 @@ fn rule_lines_store_what_onenote_stores() { | ... | @@ -2957,3 +2958,20 @@ fn rule_lines_store_what_onenote_stores() { |
| 2957 | .unwrap(); | 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,8 +13,9 @@ use std::collections::{BTreeMap, BTreeSet}; |
| 13 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] | 13 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] |
| 14 | #[serde(deny_unknown_fields)] | 14 | #[serde(deny_unknown_fields)] |
| 15 | pub enum OutlineEdit { | 15 | pub enum OutlineEdit { |
| 16 | /// Moves an ordinary page outline to coordinates measured in points, or offsets a page's | 16 | /// Moves an ordinary page outline or a file on the page to coordinates measured in |
| 17 | /// ink drawing from its strokes' coordinates by that much, as OneNote 2010 moves one. | 17 | /// points, or offsets a page's ink drawing from its strokes' coordinates by that much, |
| 18 | /// as OneNote 2010 moves one. | ||
| 18 | /// Refreshes automatic page titles when the leading outline changes. | 19 | /// Refreshes automatic page titles when the leading outline changes. |
| 19 | Position { x: f32, y: f32 }, | 20 | Position { x: f32, y: f32 }, |
| 20 | /// Changes an ordinary outline's maximum width in points, at least 36. | 21 | /// Changes an ordinary outline's maximum width in points, at least 36. |
| ... | @@ -65,12 +66,14 @@ impl OutlineEdit { | ... | @@ -65,12 +66,14 @@ impl OutlineEdit { |
| 65 | } | 66 | } |
| 66 | } | 67 | } |
| 67 | Self::Position { .. } => { | 68 | Self::Position { .. } => { |
| 68 | if !matches!(node.kind, Kind::Outline { .. } | Kind::Ink { .. }) | 69 | if !matches!( |
| 69 | || parents.get(&object).map(Vec::as_slice) != Some(pages.as_slice()) | 70 | node.kind, |
| 71 | Kind::Outline { .. } | Kind::Ink { .. } | Kind::Attachment { .. } | ||
| 72 | ) || parents.get(&object).map(Vec::as_slice) != Some(pages.as_slice()) | ||
| 70 | || !view.nodes[page].children.contains(&object) | 73 | || !view.nodes[page].children.contains(&object) |
| 71 | { | 74 | { |
| 72 | return Err(invalid( | 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,6 +179,8 @@ pub enum PageObject { |
| 179 | Outline(Outline), | 179 | Outline(Outline), |
| 180 | Title(Title), | 180 | Title(Title), |
| 181 | Image(Image), | 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 | Ink(Ink), | 184 | Ink(Ink), |
| 183 | Unsupported(Unsupported), | 185 | Unsupported(Unsupported), |
| 184 | } | 186 | } |
| ... | @@ -189,6 +191,7 @@ impl PageObject { | ... | @@ -189,6 +191,7 @@ impl PageObject { |
| 189 | Self::Outline(value) => value.id, | 191 | Self::Outline(value) => value.id, |
| 190 | Self::Title(value) => value.id, | 192 | Self::Title(value) => value.id, |
| 191 | Self::Image(value) => value.id, | 193 | Self::Image(value) => value.id, |
| 194 | Self::Attachment(value) => value.id, | ||
| 192 | Self::Ink(value) => value.id, | 195 | Self::Ink(value) => value.id, |
| 193 | Self::Unsupported(value) => value.id, | 196 | Self::Unsupported(value) => value.id, |
| 194 | } | 197 | } |
| ... | @@ -198,6 +201,7 @@ impl PageObject { | ... | @@ -198,6 +201,7 @@ impl PageObject { |
| 198 | Self::Outline(value) => &value.layout, | 201 | Self::Outline(value) => &value.layout, |
| 199 | Self::Title(value) => &value.layout, | 202 | Self::Title(value) => &value.layout, |
| 200 | Self::Image(value) => &value.layout, | 203 | Self::Image(value) => &value.layout, |
| 204 | Self::Attachment(value) => &value.layout, | ||
| 201 | Self::Ink(value) => &value.layout, | 205 | Self::Ink(value) => &value.layout, |
| 202 | Self::Unsupported(value) => &value.layout, | 206 | Self::Unsupported(value) => &value.layout, |
| 203 | } | 207 | } |
| ... | @@ -208,6 +212,7 @@ impl PageObject { | ... | @@ -208,6 +212,7 @@ impl PageObject { |
| 208 | Self::Outline(value) => &mut value.layout, | 212 | Self::Outline(value) => &mut value.layout, |
| 209 | Self::Title(value) => &mut value.layout, | 213 | Self::Title(value) => &mut value.layout, |
| 210 | Self::Image(value) => &mut value.layout, | 214 | Self::Image(value) => &mut value.layout, |
| 215 | Self::Attachment(value) => &mut value.layout, | ||
| 211 | Self::Ink(value) => &mut value.layout, | 216 | Self::Ink(value) => &mut value.layout, |
| 212 | Self::Unsupported(value) => &mut value.layout, | 217 | Self::Unsupported(value) => &mut value.layout, |
| 213 | } | 218 | } |
| ... | @@ -268,8 +273,11 @@ pub struct Attachment { | ... | @@ -268,8 +273,11 @@ pub struct Attachment { |
| 268 | pub filename: String, | 273 | pub filename: String, |
| 269 | /// Where the file was inserted from, when recorded. | 274 | /// Where the file was inserted from, when recorded. |
| 270 | pub source_path: Option<String>, | 275 | pub source_path: Option<String>, |
| 271 | /// The icon OneNote rendered for the file, when present. | 276 | /// The icon's size in points. |
| 272 | pub size: Option<[f32; 2]>, | 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 | #[serde(with = "payload")] | 281 | #[serde(with = "payload")] |
| 274 | pub bytes: Option<Arc<[u8]>>, | 282 | pub bytes: Option<Arc<[u8]>>, |
| 275 | #[serde(with = "payload")] | 283 | #[serde(with = "payload")] |
| ... | @@ -301,6 +309,7 @@ impl PartialEq for Attachment { | ... | @@ -301,6 +309,7 @@ impl PartialEq for Attachment { |
| 301 | && self.filename == other.filename | 309 | && self.filename == other.filename |
| 302 | && self.source_path == other.source_path | 310 | && self.source_path == other.source_path |
| 303 | && self.size == other.size | 311 | && self.size == other.size |
| 312 | && self.layout == other.layout | ||
| 304 | && self.recording == other.recording | 313 | && self.recording == other.recording |
| 305 | } | 314 | } |
| 306 | } | 315 | } |
| ... | @@ -343,6 +352,7 @@ impl Attachment { | ... | @@ -343,6 +352,7 @@ impl Attachment { |
| 343 | filename: filename.clone().unwrap_or_default(), | 352 | filename: filename.clone().unwrap_or_default(), |
| 344 | source_path: source_path.clone(), | 353 | source_path: source_path.clone(), |
| 345 | size: icon_width.zip(*icon_height).map(|(w, h)| [w, h]), | 354 | size: icon_width.zip(*icon_height).map(|(w, h)| [w, h]), |
| 355 | layout: node.layout.clone(), | ||
| 346 | bytes: payload(container)?, | 356 | bytes: payload(container)?, |
| 347 | preview: payload(preview)?, | 357 | preview: payload(preview)?, |
| 348 | recording: recording_id.map(|id| Recording { | 358 | recording: recording_id.map(|id| Recording { |
| ... | @@ -818,6 +828,11 @@ impl Page { | ... | @@ -818,6 +828,11 @@ impl Page { |
| 818 | page.objects | 828 | page.objects |
| 819 | .push(PageObject::Image(Image::read(revision, id, node)?)); | 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 | Kind::Ink { .. } => { | 836 | Kind::Ink { .. } => { |
| 822 | page.objects | 837 | page.objects |
| 823 | .push(PageObject::Ink(Ink::read(revision, id, node)?)); | 838 | .push(PageObject::Ink(Ink::read(revision, id, node)?)); |
crates/onestore/src/tree.rs+4-1| ... | @@ -89,7 +89,10 @@ impl TreeEdit { | ... | @@ -89,7 +89,10 @@ impl TreeEdit { |
| 89 | .get(1) | 89 | .get(1) |
| 90 | .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?; | 90 | .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?; |
| 91 | let compatible = |parent: ExGuid| match view.nodes[&self.object].kind { | 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 | Kind::Paragraph { .. } => matches!( | 96 | Kind::Paragraph { .. } => matches!( |
| 94 | view.nodes[&parent].kind, | 97 | view.nodes[&parent].kind, |
| 95 | Kind::Outline { .. } | 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,6 +112,7 @@ fn insert_attachment(section: &str, preview: Option<Arc<[u8]>>) -> Vec<u8> { |
| 112 | filename: "notes 🦀.txt".into(), | 112 | filename: "notes 🦀.txt".into(), |
| 113 | source_path: None, | 113 | source_path: None, |
| 114 | size: Some([24.0, 24.0]), | 114 | size: Some([24.0, 24.0]), |
| 115 | layout: Default::default(), | ||
| 115 | bytes: Some(Arc::from(PAYLOAD)), | 116 | bytes: Some(Arc::from(PAYLOAD)), |
| 116 | preview: preview.clone(), | 117 | preview: preview.clone(), |
| 117 | recording: None, | 118 | recording: None, |
| ... | @@ -182,6 +183,7 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { | ... | @@ -182,6 +183,7 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { |
| 182 | filename: "sub/dir.txt".into(), | 183 | filename: "sub/dir.txt".into(), |
| 183 | source_path: None, | 184 | source_path: None, |
| 184 | size: None, | 185 | size: None, |
| 186 | layout: Default::default(), | ||
| 185 | bytes: Some(Arc::from(PAYLOAD)), | 187 | bytes: Some(Arc::from(PAYLOAD)), |
| 186 | preview: None, | 188 | preview: None, |
| 187 | recording: None, | 189 | recording: None, |
| ... | @@ -230,3 +232,139 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { | ... | @@ -230,3 +232,139 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { |
| 230 | .unwrap(); | 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,7 +60,10 @@ fn identities(page: &Page) -> Vec<onestore::ExGuid> { |
| 60 | ids.extend(outline.unsupported.iter().map(|u| u.id)); | 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 | ids | 69 | ids |
crates/onestore/tests/protected.rs+1| ... | @@ -175,6 +175,7 @@ fn a_protected_page_edit_is_stored_under_the_section_key() { | ... | @@ -175,6 +175,7 @@ fn a_protected_page_edit_is_stored_under_the_section_key() { |
| 175 | filename: "sealed.txt".into(), | 175 | filename: "sealed.txt".into(), |
| 176 | source_path: None, | 176 | source_path: None, |
| 177 | size: Some([24.0, 24.0]), | 177 | size: Some([24.0, 24.0]), |
| 178 | layout: Default::default(), | ||
| 178 | bytes: Some(std::sync::Arc::from(&b"A payload that stays sealed"[..])), | 179 | bytes: Some(std::sync::Arc::from(&b"A payload that stays sealed"[..])), |
| 179 | preview: None, | 180 | preview: None, |
| 180 | recording: None, | 181 | recording: None, |
crates/onestore/tests/support/page_edits.rs+1| ... | @@ -142,6 +142,7 @@ fn projection(page: &Page) -> String { | ... | @@ -142,6 +142,7 @@ fn projection(page: &Page) -> String { |
| 142 | } | 142 | } |
| 143 | PageObject::Title(title) => out.push_str(&format!("title {}\n", title.id)), | 143 | PageObject::Title(title) => out.push_str(&format!("title {}\n", title.id)), |
| 144 | PageObject::Image(image) => out.push_str(&format!("image {}\n", image.id)), | 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 | PageObject::Ink(ink) => out.push_str(&format!("ink {}\n", ink.id)), | 146 | PageObject::Ink(ink) => out.push_str(&format!("ink {}\n", ink.id)), |
| 146 | PageObject::Unsupported(u) => out.push_str(&format!("unsupported {}\n", u.id)), | 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,6 +66,3 @@ pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy"); |
| 66 | pub const SYNC_DONE: &[&str] = art!("icons/sync-done"); | 66 | pub const SYNC_DONE: &[&str] = art!("icons/sync-done"); |
| 67 | pub const SYNC_ERROR: &[&str] = art!("icons/sync-error"); | 67 | pub const SYNC_ERROR: &[&str] = art!("icons/sync-error"); |
| 68 | pub const SYNC_OFFLINE: &[&str] = art!("icons/sync-offline"); | 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,7 +2,7 @@ |
| 2 | //! chords on each platform, when it applies and what it does. | 2 | //! chords on each platform, when it applies and what it does. |
| 3 | 3 | ||
| 4 | use crate::{Command as Work, FONTS, HIGHLIGHTS, SIZES, State, page, platform, search}; | 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 | use canvas::interaction::Request; | 6 | use canvas::interaction::Request; |
| 7 | use draw::edit::{Key, Modifiers, NamedKey, Platform}; | 7 | use draw::edit::{Key, Modifiers, NamedKey, Platform}; |
| 8 | use std::{error::Error, sync::Arc}; | 8 | use std::{error::Error, sync::Arc}; |
| ... | @@ -63,7 +63,9 @@ pub enum Id { | ... | @@ -63,7 +63,9 @@ pub enum Id { |
| 63 | Indent, | 63 | Indent, |
| 64 | Outdent, | 64 | Outdent, |
| 65 | ClearFormatting, | 65 | ClearFormatting, |
| 66 | Tag(NoteTag), | 66 | /// The tag at this place in the user's tag list. |
| 67 | Tag(usize), | ||
| 68 | CustomizeTags, | ||
| 67 | RemoveTags, | 69 | RemoveTags, |
| 68 | FindTags, | 70 | FindTags, |
| 69 | Help, | 71 | Help, |
| ... | @@ -176,15 +178,6 @@ macro_rules! row { | ... | @@ -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 | const NONE: &[Chord] = &[]; | 181 | const NONE: &[Chord] = &[]; |
| 189 | 182 | ||
| 190 | /// OneNote 2010's chords off macOS; there, AppKit's and OneNote for Mac's. | 183 | /// OneNote 2010's chords off macOS; there, AppKit's and OneNote for Mac's. |
| ... | @@ -378,35 +371,7 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -378,35 +371,7 @@ pub const COMMANDS: &[Command] = &[ |
| 378 | &[cmd('n').shift()], | 371 | &[cmd('n').shift()], |
| 379 | &[cmd('n').shift()] | 372 | &[cmd('n').shift()] |
| 380 | ), | 373 | ), |
| 381 | tag!(NoteTag::ToDo, '1'), | 374 | row!(Id::CustomizeTags, "Customize Tags…", NONE, NONE), |
| 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), | ||
| 410 | row!( | 375 | row!( |
| 411 | Id::RemoveTags, | 376 | Id::RemoveTags, |
| 412 | "Remove Tag", | 377 | "Remove Tag", |
| ... | @@ -417,6 +382,9 @@ pub const COMMANDS: &[Command] = &[ | ... | @@ -417,6 +382,9 @@ pub const COMMANDS: &[Command] = &[ |
| 417 | row!(Id::Help, "Snowbound Help", NONE, NONE), | 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 | /// Where Snowbound Help leads. | 388 | /// Where Snowbound Help leads. |
| 421 | const HELP: &str = "https://shale.paperclover.net/snowbound"; | 389 | const HELP: &str = "https://shale.paperclover.net/snowbound"; |
| 422 | 390 | ||
| ... | @@ -443,6 +411,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { | ... | @@ -443,6 +411,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { |
| 443 | .map(|command| Choice::Command(command.id)) | 411 | .map(|command| Choice::Command(command.id)) |
| 444 | .chain(FONTS.map(|name| Choice::Font(name.to_owned()))) | 412 | .chain(FONTS.map(|name| Choice::Font(name.to_owned()))) |
| 445 | .chain(SIZES.into_iter().map(Choice::Size)) | 413 | .chain(SIZES.into_iter().map(Choice::Size)) |
| 414 | .chain((0..TAGS).map(|place| Choice::Command(Id::Tag(place)))) | ||
| 446 | .chain(std::iter::once(Choice::Highlight(None))) | 415 | .chain(std::iter::once(Choice::Highlight(None))) |
| 447 | .chain( | 416 | .chain( |
| 448 | HIGHLIGHTS | 417 | HIGHLIGHTS |
| ... | @@ -455,6 +424,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { | ... | @@ -455,6 +424,7 @@ pub fn choices() -> impl Iterator<Item = Choice> { |
| 455 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] | 424 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] |
| 456 | pub fn title(choice: Choice) -> String { | 425 | pub fn title(choice: Choice) -> String { |
| 457 | match choice { | 426 | match choice { |
| 427 | Choice::Command(Id::Tag(_)) => unreachable!("The tag list names its tags"), | ||
| 458 | Choice::Command(id) => command(id).title.to_owned(), | 428 | Choice::Command(id) => command(id).title.to_owned(), |
| 459 | Choice::Font(name) => name, | 429 | Choice::Font(name) => name, |
| 460 | Choice::Size(size) => format!("{size}"), | 430 | Choice::Size(size) => format!("{size}"), |
| ... | @@ -483,11 +453,30 @@ pub fn index(choice: &Choice) -> usize { | ... | @@ -483,11 +453,30 @@ pub fn index(choice: &Choice) -> usize { |
| 483 | 453 | ||
| 484 | /// The command `key` with `modifiers` runs here. | 454 | /// The command `key` with `modifiers` runs here. |
| 485 | pub fn find(key: &Key, modifiers: Modifiers) -> Option<Id> { | 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 | COMMANDS | 464 | COMMANDS |
| 488 | .iter() | 465 | .iter() |
| 489 | .find(|command| command.chords(Platform::CURRENT).contains(&pressed)) | 466 | .find(|command| command.chords(platform).contains(&chord)) |
| 490 | .map(|command| command.id) | 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 | fn pressed(key: &Key, modifiers: Modifiers, platform: Platform) -> Option<Chord> { | 482 | fn pressed(key: &Key, modifiers: Modifiers, platform: Platform) -> Option<Chord> { |
| ... | @@ -599,10 +588,11 @@ impl Chord { | ... | @@ -599,10 +588,11 @@ impl Chord { |
| 599 | 588 | ||
| 600 | /// The chord a menu or hint shows for `id` here, empty where it has none. | 589 | /// The chord a menu or hint shows for `id` here, empty where it has none. |
| 601 | pub fn shortcut(id: Id) -> String { | 590 | pub fn shortcut(id: Id) -> String { |
| 602 | command(id) | 591 | let chord = match id { |
| 603 | .chords(Platform::CURRENT) | 592 | Id::Tag(place) => tag_chord(place), |
| 604 | .first() | 593 | id => command(id).chords(Platform::CURRENT).first().copied(), |
| 605 | .map_or_else(String::new, |chord| chord.label(Platform::CURRENT)) | 594 | }; |
| 595 | chord.map_or_else(String::new, |chord| chord.label(Platform::CURRENT)) | ||
| 606 | } | 596 | } |
| 607 | 597 | ||
| 608 | impl State { | 598 | impl State { |
| ... | @@ -627,7 +617,7 @@ impl State { | ... | @@ -627,7 +617,7 @@ impl State { |
| 627 | pub(crate) fn status(&self, choice: &Choice, format: &FormatState) -> Status { | 617 | pub(crate) fn status(&self, choice: &Choice, format: &FormatState) -> Status { |
| 628 | let session = self.session.as_ref(); | 618 | let session = self.session.as_ref(); |
| 629 | let welcome = session.is_none() && !self.temporary && self.sectionless.is_none(); | 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 | let page = (session.is_some() || self.temporary) && !modal; | 621 | let page = (session.is_some() || self.temporary) && !modal; |
| 632 | let writable = page && !session.is_some_and(|session| session.read_only()); | 622 | let writable = page && !session.is_some_and(|session| session.read_only()); |
| 633 | let text = writable && self.view.accepts_text(); | 623 | let text = writable && self.view.accepts_text(); |
| ... | @@ -655,9 +645,12 @@ impl State { | ... | @@ -655,9 +645,12 @@ impl State { |
| 655 | Choice::PageColor(_) | Choice::RuleLines(_) | Choice::Art(_) => Id::PageColor, | 645 | Choice::PageColor(_) | Choice::RuleLines(_) | Choice::Art(_) => Id::PageColor, |
| 656 | }; | 646 | }; |
| 657 | match id { | 647 | match id { |
| 658 | Id::Settings | Id::NewNotebook | Id::OpenNotebook | Id::CommandPalette | Id::Help => { | 648 | Id::Settings |
| 659 | enabled(!modal) | 649 | | Id::NewNotebook |
| 660 | } | 650 | | Id::OpenNotebook |
| 651 | | Id::CommandPalette | ||
| 652 | | Id::CustomizeTags | ||
| 653 | | Id::Help => enabled(!modal), | ||
| 661 | Id::CloseNotebook | Id::ShowNotebook => enabled(!modal && self.notebook().is_some()), | 654 | Id::CloseNotebook | Id::ShowNotebook => enabled(!modal && self.notebook().is_some()), |
| 662 | Id::NewSection | Id::NewSectionGroup => enabled( | 655 | Id::NewSection | Id::NewSectionGroup => enabled( |
| 663 | !modal | 656 | !modal |
| ... | @@ -715,14 +708,15 @@ impl State { | ... | @@ -715,14 +708,15 @@ impl State { |
| 715 | }, | 708 | }, |
| 716 | Id::PageColor => enabled(writable && session.is_some()), | 709 | Id::PageColor => enabled(writable && session.is_some()), |
| 717 | Id::ScreenClipping => enabled(text && cfg!(target_os = "macos")), | 710 | Id::ScreenClipping => enabled(text && cfg!(target_os = "macos")), |
| 718 | // Attach File awaits #23, Record Audio and Video #36. | 711 | // Record Audio and Video await #36. |
| 719 | Id::Attachment | Id::RecordAudio | Id::RecordVideo => enabled(false), | 712 | Id::RecordAudio | Id::RecordVideo => enabled(false), |
| 720 | Id::InsertSpace => Status { | 713 | Id::InsertSpace => Status { |
| 721 | enabled: writable, | 714 | enabled: writable, |
| 722 | checked: self.view.inserting_space(), | 715 | checked: self.view.inserting_space(), |
| 723 | }, | 716 | }, |
| 724 | Id::Table | 717 | Id::Table |
| 725 | | Id::Picture | 718 | | Id::Picture |
| 719 | | Id::Attachment | ||
| 726 | | Id::Symbol | 720 | | Id::Symbol |
| 727 | | Id::Link | 721 | | Id::Link |
| 728 | | Id::Equation | 722 | | Id::Equation |
| ... | @@ -739,7 +733,10 @@ impl State { | ... | @@ -739,7 +733,10 @@ impl State { |
| 739 | Id::Bullets => checked(format.bullets), | 733 | Id::Bullets => checked(format.bullets), |
| 740 | Id::Numbering => checked(format.numbering), | 734 | Id::Numbering => checked(format.numbering), |
| 741 | Id::Align(alignment) => checked(format.alignment == Some(alignment)), | 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,14 +812,19 @@ impl State { |
| 815 | return Ok(()); | 812 | return Ok(()); |
| 816 | } | 813 | } |
| 817 | Choice::PageColor(color) => { | 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 | Choice::Art(name) => { | 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 | Choice::RuleLines(lines) => { | 825 | Choice::RuleLines(lines) => { |
| 824 | let lines = lines.map(|index| canvas::template::RULE_LINES[index].1); | 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 | let field = self.ui.focused().filter(|focus| *focus != page()); | 830 | let field = self.ui.focused().filter(|focus| *focus != page()); |
| ... | @@ -951,11 +953,18 @@ impl State { | ... | @@ -951,11 +953,18 @@ impl State { |
| 951 | return Ok(()); | 953 | return Ok(()); |
| 952 | } | 954 | } |
| 953 | Id::Picture => { | 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 | return Ok(()); | 958 | return Ok(()); |
| 956 | }; | 959 | }; |
| 957 | return self.insert_picture(std::fs::read(path)?); | 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 | Id::ScreenClipping => { | 968 | Id::ScreenClipping => { |
| 960 | #[cfg(target_os = "macos")] | 969 | #[cfg(target_os = "macos")] |
| 961 | platform::clip_screen(self.proxy.clone()); | 970 | platform::clip_screen(self.proxy.clone()); |
| ... | @@ -971,7 +980,7 @@ impl State { | ... | @@ -971,7 +980,7 @@ impl State { |
| 971 | self.save_settings(); | 980 | self.save_settings(); |
| 972 | return Ok(()); | 981 | return Ok(()); |
| 973 | } | 982 | } |
| 974 | Id::Attachment | Id::RecordAudio | Id::RecordVideo => { | 983 | Id::RecordAudio | Id::RecordVideo => { |
| 975 | return Ok(()); | 984 | return Ok(()); |
| 976 | } | 985 | } |
| 977 | Id::InsertSpace => { | 986 | Id::InsertSpace => { |
| ... | @@ -1016,7 +1025,14 @@ impl State { | ... | @@ -1016,7 +1025,14 @@ impl State { |
| 1016 | Id::Indent => return format(self, Formatting::Indent), | 1025 | Id::Indent => return format(self, Formatting::Indent), |
| 1017 | Id::Outdent => return format(self, Formatting::Outdent), | 1026 | Id::Outdent => return format(self, Formatting::Outdent), |
| 1018 | Id::ClearFormatting => return format(self, Formatting::Clear), | 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 | Id::RemoveTags => return format(self, Formatting::RemoveTags), | 1036 | Id::RemoveTags => return format(self, Formatting::RemoveTags), |
| 1021 | }; | 1037 | }; |
| 1022 | self.commands.push(response); | 1038 | self.commands.push(response); |
| ... | @@ -1039,8 +1055,9 @@ mod tests { | ... | @@ -1039,8 +1055,9 @@ mod tests { |
| 1039 | command | 1055 | command |
| 1040 | .chords(platform) | 1056 | .chords(platform) |
| 1041 | .iter() | 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 | .collect(); | 1061 | .collect(); |
| 1045 | for (at, (chord, title)) in chords.iter().enumerate() { | 1062 | for (at, (chord, title)) in chords.iter().enumerate() { |
| 1046 | if let Some((_, other)) = | 1063 | if let Some((_, other)) = |
| ... | @@ -1081,11 +1098,10 @@ mod tests { | ... | @@ -1081,11 +1098,10 @@ mod tests { |
| 1081 | control, | 1098 | control, |
| 1082 | command, | 1099 | command, |
| 1083 | }; | 1100 | }; |
| 1084 | let pressed = pressed(&Key::Character(key.into()), modifiers, platform)?; | 1101 | ran( |
| 1085 | COMMANDS | 1102 | pressed(&Key::Character(key.into()), modifiers, platform)?, |
| 1086 | .iter() | 1103 | platform, |
| 1087 | .find(|listed| listed.chords(platform).contains(&pressed)) | 1104 | ) |
| 1088 | .map(|listed| listed.id) | ||
| 1089 | }; | 1105 | }; |
| 1090 | let mac = Platform::MacOs; | 1106 | let mac = Platform::MacOs; |
| 1091 | assert_eq!( | 1107 | assert_eq!( |
| ... | @@ -1110,10 +1126,7 @@ mod tests { | ... | @@ -1110,10 +1126,7 @@ mod tests { |
| 1110 | find(mac, "0", false, false, true, true), | 1126 | find(mac, "0", false, false, true, true), |
| 1111 | Some(Id::RemoveTags) | 1127 | Some(Id::RemoveTags) |
| 1112 | ); | 1128 | ); |
| 1113 | assert_eq!( | 1129 | assert_eq!(find(mac, "7", false, false, false, true), Some(Id::Tag(6))); |
| 1114 | find(mac, "7", false, false, false, true), | ||
| 1115 | Some(Id::Tag(NoteTag::Contact)) | ||
| 1116 | ); | ||
| 1117 | assert_eq!(find(mac, "q", false, false, false, true), None); | 1130 | assert_eq!(find(mac, "q", false, false, false, true), None); |
| 1118 | // The highlighter's chord runs the command that applies its last pick. | 1131 | // The highlighter's chord runs the command that applies its last pick. |
| 1119 | assert_eq!( | 1132 | assert_eq!( |
crates/snowbound/src/link.rs+48-40| ... | @@ -275,7 +275,7 @@ impl State { | ... | @@ -275,7 +275,7 @@ impl State { |
| 275 | Ok(()) | 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 | pub(crate) fn open_text_menu(&mut self) -> Result<(), Box<dyn Error>> { | 279 | pub(crate) fn open_text_menu(&mut self) -> Result<(), Box<dyn Error>> { |
| 280 | let Some((response, context)) = self.view.context()? else { | 280 | let Some((response, context)) = self.view.context()? else { |
| 281 | return Ok(()); | 281 | return Ok(()); |
| ... | @@ -288,7 +288,7 @@ impl State { | ... | @@ -288,7 +288,7 @@ impl State { |
| 288 | } | 288 | } |
| 289 | 289 | ||
| 290 | /// The page's context menu while it is open, with OneNote 2010's commands for text, | 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 | pub(crate) fn text_menu(&mut self) -> Result<(), Box<dyn Error>> { | 292 | pub(crate) fn text_menu(&mut self) -> Result<(), Box<dyn Error>> { |
| 293 | let Some((context, point)) = &self.text_menu else { | 293 | let Some((context, point)) = &self.text_menu else { |
| 294 | return Ok(()); | 294 | return Ok(()); |
| ... | @@ -301,48 +301,54 @@ impl State { | ... | @@ -301,48 +301,54 @@ impl State { |
| 301 | text, | 301 | text, |
| 302 | ..Item::default() | 302 | ..Item::default() |
| 303 | }; | 303 | }; |
| 304 | let mut items = vec![ | 304 | let items = if context.attachment.is_some() { |
| 305 | Item { | 305 | // OneNote 2010's commands for the file itself; its clipboard holds text alone. |
| 306 | disabled: !context.selected, | 306 | vec![item("Open"), item("Save As…")] |
| 307 | ..item("Cut") | 307 | } else { |
| 308 | }, | 308 | let mut items = vec![ |
| 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([ | ||
| 330 | Item { | 309 | Item { |
| 331 | separated: true, | 310 | disabled: !context.selected, |
| 332 | ..item("Link…") | 311 | ..item("Cut") |
| 333 | }, | 312 | }, |
| 334 | item("Copy Link to Paragraph"), | ||
| 335 | ]), | ||
| 336 | } | ||
| 337 | if context.equation { | ||
| 338 | items.extend([ | ||
| 339 | Item { | 313 | Item { |
| 340 | separated: true, | 314 | disabled: !context.selected, |
| 341 | ..item("Professional") | 315 | ..item("Copy") |
| 342 | }, | 316 | }, |
| 343 | item("Linear"), | 317 | item("Paste"), |
| 344 | ]); | 318 | ]; |
| 345 | } | 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 | let Some(chosen) = | 352 | let Some(chosen) = |
| 347 | ui::popup::menu(&mut self.ui, menu(), Anchor::Point(*point), &items, None) | 353 | ui::popup::menu(&mut self.ui, menu(), Anchor::Point(*point), &items, None) |
| 348 | else { | 354 | else { |
| ... | @@ -352,6 +358,8 @@ impl State { | ... | @@ -352,6 +358,8 @@ impl State { |
| 352 | return Ok(()); | 358 | return Ok(()); |
| 353 | }; | 359 | }; |
| 354 | let response = match items[chosen].text { | 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 | "Cut" => self.view.copy(true)?, | 363 | "Cut" => self.view.copy(true)?, |
| 356 | "Copy" => self.view.copy(false)?, | 364 | "Copy" => self.view.copy(false)?, |
| 357 | "Paste" => { | 365 | "Paste" => { |
crates/snowbound/src/linux.rs+32-16| ... | @@ -593,6 +593,8 @@ pub fn install_menu() {} | ... | @@ -593,6 +593,8 @@ pub fn install_menu() {} |
| 593 | /// The toolbar and keyboard stand in for a menu bar. | 593 | /// The toolbar and keyboard stand in for a menu bar. |
| 594 | pub fn update_menu(_: impl FnOnce() -> Vec<crate::commands::Status>) {} | 594 | pub fn update_menu(_: impl FnOnce() -> Vec<crate::commands::Status>) {} |
| 595 | 595 | ||
| 596 | pub fn update_tag_menu(_: &[canvas::editor::NoteTag]) {} | ||
| 597 | |||
| 596 | pub fn clear_marked_text(_: &Window) {} | 598 | pub fn clear_marked_text(_: &Window) {} |
| 597 | 599 | ||
| 598 | #[cfg(feature = "wgpu")] | 600 | #[cfg(feature = "wgpu")] |
| ... | @@ -1056,24 +1058,23 @@ fn dialog<const Z: usize, const K: usize>( | ... | @@ -1056,24 +1058,23 @@ fn dialog<const Z: usize, const K: usize>( |
| 1056 | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) | 1058 | .then(|| String::from_utf8_lossy(&output.stdout).trim().to_owned())) |
| 1057 | } | 1059 | } |
| 1058 | 1060 | ||
| 1059 | /// Asks for a picture to insert. | 1061 | /// Asks for a file to insert, one of `types` (extensions) unless empty, titled `title`. |
| 1060 | pub fn pick_picture() -> Option<PathBuf> { | 1062 | pub fn pick_file(title: &str, types: &[&str]) -> Option<PathBuf> { |
| 1061 | let patterns = crate::PICTURE_TYPES | 1063 | let patterns = match types { |
| 1062 | .map(|kind| format!("*.{kind}")) | 1064 | [] => "*".to_owned(), |
| 1063 | .join(" "); | 1065 | types => types |
| 1066 | .iter() | ||
| 1067 | .map(|kind| format!("*.{kind}")) | ||
| 1068 | .collect::<Vec<_>>() | ||
| 1069 | .join(" "), | ||
| 1070 | }; | ||
| 1064 | let asked = dialog( | 1071 | let asked = dialog( |
| 1065 | [ | 1072 | [ |
| 1066 | "--file-selection", | 1073 | "--file-selection", |
| 1067 | "--title=Insert Picture", | 1074 | &format!("--title={title}"), |
| 1068 | &format!("--file-filter=Pictures | {patterns}"), | 1075 | &format!("--file-filter={patterns}"), |
| 1069 | ], | ||
| 1070 | [ | ||
| 1071 | "--getopenfilename", | ||
| 1072 | ".", | ||
| 1073 | &patterns, | ||
| 1074 | "--title", | ||
| 1075 | "Insert Picture", | ||
| 1076 | ], | 1076 | ], |
| 1077 | ["--getopenfilename", ".", &patterns, "--title", title], | ||
| 1077 | ); | 1078 | ); |
| 1078 | asked | 1079 | asked |
| 1079 | .unwrap_or_else(|error| { | 1080 | .unwrap_or_else(|error| { |
| ... | @@ -1084,6 +1085,21 @@ pub fn pick_picture() -> Option<PathBuf> { | ... | @@ -1084,6 +1085,21 @@ pub fn pick_picture() -> Option<PathBuf> { |
| 1084 | .map(PathBuf::from) | 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 | /// Opens GNOME's character map, whose picks the user copies in. | 1103 | /// Opens GNOME's character map, whose picks the user copies in. |
| 1088 | pub fn character_palette() { | 1104 | pub fn character_palette() { |
| 1089 | if let Err(error) = Command::new("gnome-characters").spawn() { | 1105 | if let Err(error) = Command::new("gnome-characters").spawn() { |
| ... | @@ -1151,8 +1167,8 @@ pub fn pick_notebook(title: &str) -> Option<PathBuf> { | ... | @@ -1151,8 +1167,8 @@ pub fn pick_notebook(title: &str) -> Option<PathBuf> { |
| 1151 | .map(PathBuf::from) | 1167 | .map(PathBuf::from) |
| 1152 | } | 1168 | } |
| 1153 | 1169 | ||
| 1154 | /// Asks where to create something named `name` by default. | 1170 | /// Asks where to put something named `name` by default; the dialog names its own button. |
| 1155 | pub fn pick_new(title: &str, name: &str) -> Option<PathBuf> { | 1171 | pub fn pick_new(title: &str, name: &str, _action: &str) -> Option<PathBuf> { |
| 1156 | let asked = dialog( | 1172 | let asked = dialog( |
| 1157 | [ | 1173 | [ |
| 1158 | "--file-selection", | 1174 | "--file-selection", |
crates/snowbound/src/macos.rs+108-9| ... | @@ -11,7 +11,7 @@ use objc2::{ | ... | @@ -11,7 +11,7 @@ use objc2::{ |
| 11 | use objc2_app_kit::{ | 11 | use objc2_app_kit::{ |
| 12 | NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSApplication, NSColor, | 12 | NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSApplication, NSColor, |
| 13 | NSColorSpace, NSDatePicker, NSDatePickerElementFlags, NSDatePickerStyle, NSEvent, NSEventType, | 13 | NSColorSpace, NSDatePicker, NSDatePickerElementFlags, NSDatePickerStyle, NSEvent, NSEventType, |
| 14 | NSMenuItem, | 14 | NSMenu, NSMenuItem, |
| 15 | }; | 15 | }; |
| 16 | use objc2_foundation::{ | 16 | use objc2_foundation::{ |
| 17 | MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter, | 17 | MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter, |
| ... | @@ -38,6 +38,7 @@ static DELEGATE_CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); | ... | @@ -38,6 +38,7 @@ static DELEGATE_CLASS: OnceLock<&'static AnyClass> = OnceLock::new(); |
| 38 | thread_local! { | 38 | thread_local! { |
| 39 | static IN_KEY_DOWN: Cell<bool> = const { Cell::new(false) }; | 39 | static IN_KEY_DOWN: Cell<bool> = const { Cell::new(false) }; |
| 40 | static STATUSES: RefCell<Vec<commands::Status>> = const { RefCell::new(Vec::new()) }; | 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 | unsafe extern "C" fn key_down(view: &AnyObject, _: Sel, event: &NSEvent) { | 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,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 | /// Asks where to `action` something named `name` by default, with the system's save panel. |
| 431 | pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { | 432 | pub fn pick_new(title: &str, name: &str, action: &str) -> Option<std::path::PathBuf> { |
| 432 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); | 433 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); |
| 433 | unsafe { | 434 | unsafe { |
| 434 | let panel = objc2_app_kit::NSSavePanel::savePanel(mtm); | 435 | let panel = objc2_app_kit::NSSavePanel::savePanel(mtm); |
| 435 | panel.setCanCreateDirectories(true); | 436 | panel.setCanCreateDirectories(true); |
| 436 | panel.setTitle(Some(&NSString::from_str(title))); | 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 | panel.setNameFieldStringValue(&NSString::from_str(name)); | 439 | panel.setNameFieldStringValue(&NSString::from_str(name)); |
| 439 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { | 440 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { |
| 440 | return None; | 441 | return None; |
| ... | @@ -443,14 +444,18 @@ pub fn pick_new(title: &str, name: &str) -> Option<std::path::PathBuf> { | ... | @@ -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 | /// Asks for a file to insert, one of `types` (extensions) unless empty, with the system's |
| 447 | pub fn pick_picture() -> Option<std::path::PathBuf> { | 448 | /// open panel titled `title`. |
| 449 | pub fn pick_file(title: &str, types: &[&str]) -> Option<std::path::PathBuf> { | ||
| 448 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); | 450 | let mtm = MainThreadMarker::new().expect("Panels belong to the main thread"); |
| 449 | unsafe { | 451 | unsafe { |
| 450 | let panel = objc2_app_kit::NSOpenPanel::openPanel(mtm); | 452 | let panel = objc2_app_kit::NSOpenPanel::openPanel(mtm); |
| 451 | let types = crate::PICTURE_TYPES.map(NSString::from_str); | 453 | if !types.is_empty() { |
| 452 | #[allow(deprecated, reason = "Allowed content types need macOS 11")] | 454 | let types = types.iter().map(|kind| NSString::from_str(kind)).collect(); |
| 453 | panel.setAllowedFileTypes(Some(&objc2_foundation::NSArray::from_vec(types.into()))); | 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 | panel.setPrompt(Some(&NSString::from_str("Insert"))); | 459 | panel.setPrompt(Some(&NSString::from_str("Insert"))); |
| 455 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { | 460 | if panel.runModal() != objc2_app_kit::NSModalResponseOK { |
| 456 | return None; | 461 | return None; |
| ... | @@ -460,6 +465,88 @@ pub fn pick_picture() -> Option<std::path::PathBuf> { | ... | @@ -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 | /// Lets the user drag out part of the screen, then sends it as a PNG. | 550 | /// Lets the user drag out part of the screen, then sends it as a PNG. |
| 464 | pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { | 551 | pub fn clip_screen(proxy: EventLoopProxy<crate::UserEvent>) { |
| 465 | std::thread::spawn(move || { | 552 | std::thread::spawn(move || { |
| ... | @@ -910,6 +997,18 @@ pub fn install_menu() { | ... | @@ -910,6 +997,18 @@ pub fn install_menu() { |
| 910 | app.setWindowsMenu(Some(&menus.window)); | 997 | app.setWindowsMenu(Some(&menus.window)); |
| 911 | app.setHelpMenu(Some(&menus.help)); | 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 | /// The table's statuses, in `commands::choices` order, which the menu bar's items show. | 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,6 +1,7 @@ |
| 1 | #[cfg(target_os = "macos")] | 1 | #[cfg(target_os = "macos")] |
| 2 | mod aqua; | 2 | mod aqua; |
| 3 | mod art; | 3 | mod art; |
| 4 | mod attachment; | ||
| 4 | mod background; | 5 | mod background; |
| 5 | mod commands; | 6 | mod commands; |
| 6 | #[cfg(test)] | 7 | #[cfg(test)] |
| ... | @@ -28,6 +29,7 @@ mod sidebar; | ... | @@ -28,6 +29,7 @@ mod sidebar; |
| 28 | #[cfg_attr(not(feature = "wgpu"), path = "surface_gl.rs")] | 29 | #[cfg_attr(not(feature = "wgpu"), path = "surface_gl.rs")] |
| 29 | mod surface; | 30 | mod surface; |
| 30 | mod sync; | 31 | mod sync; |
| 32 | mod tags; | ||
| 31 | mod templates; | 33 | mod templates; |
| 32 | 34 | ||
| 33 | use canvas::gpu::page::PageScene; | 35 | use canvas::gpu::page::PageScene; |
| ... | @@ -524,6 +526,10 @@ struct State { | ... | @@ -524,6 +526,10 @@ struct State { |
| 524 | options: Option<options::Options>, | 526 | options: Option<options::Options>, |
| 525 | /// The Link dialog's fields while it is open. | 527 | /// The Link dialog's fields while it is open. |
| 526 | link: Option<link::LinkDialog>, | 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 | /// The page's context menu while it is open: what it was opened on, and where. | 533 | /// The page's context menu while it is open: what it was opened on, and where. |
| 528 | text_menu: Option<(canvas::interaction::Context, [f32; 2])>, | 534 | text_menu: Option<(canvas::interaction::Context, [f32; 2])>, |
| 529 | color_scheme: settings::ColorScheme, | 535 | color_scheme: settings::ColorScheme, |
| ... | @@ -815,6 +821,10 @@ impl State { | ... | @@ -815,6 +821,10 @@ impl State { |
| 815 | title_focus: None, | 821 | title_focus: None, |
| 816 | menu: None, | 822 | menu: None, |
| 817 | link: None, | 823 | link: None, |
| 824 | tags: stored | ||
| 825 | .tags | ||
| 826 | .unwrap_or_else(canvas::editor::NoteTag::defaults), | ||
| 827 | tag_list: None, | ||
| 818 | text_menu: None, | 828 | text_menu: None, |
| 819 | options: None, | 829 | options: None, |
| 820 | color_scheme: stored.color_scheme, | 830 | color_scheme: stored.color_scheme, |
| ... | @@ -860,6 +870,7 @@ impl State { | ... | @@ -860,6 +870,7 @@ impl State { |
| 860 | accessibility: accessibility::Accessibility::default(), | 870 | accessibility: accessibility::Accessibility::default(), |
| 861 | }; | 871 | }; |
| 862 | state.title(); | 872 | state.title(); |
| 873 | platform::update_tag_menu(&state.tags); | ||
| 863 | Ok(state) | 874 | Ok(state) |
| 864 | } | 875 | } |
| 865 | 876 | ||
| ... | @@ -930,6 +941,7 @@ impl State { | ... | @@ -930,6 +941,7 @@ impl State { |
| 930 | let (section, open_tab, open_page) = self.build()?; | 941 | let (section, open_tab, open_page) = self.build()?; |
| 931 | self.options_dialog(); | 942 | self.options_dialog(); |
| 932 | self.link_dialog()?; | 943 | self.link_dialog()?; |
| 944 | self.customize_tags(); | ||
| 933 | self.palette(); | 945 | self.palette(); |
| 934 | self.sync_popup()?; | 946 | self.sync_popup()?; |
| 935 | self.text_menu()?; | 947 | self.text_menu()?; |
| ... | @@ -1468,21 +1480,16 @@ impl State { | ... | @@ -1468,21 +1480,16 @@ impl State { |
| 1468 | /// taking notes fold last, and those with chords or a place in the menu bar first. | 1480 | /// taking notes fold last, and those with chords or a place in the menu bar first. |
| 1469 | fn tools(&mut self, theme: &Theme) { | 1481 | fn tools(&mut self, theme: &Theme) { |
| 1470 | use Entry::{Open, Rule, Run, Soon}; | 1482 | use Entry::{Open, Rule, Run, Soon}; |
| 1471 | use canvas::editor::{ | 1483 | use canvas::editor::{Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, Toggle}; |
| 1472 | Alignment, BULLET_LIBRARY, ListStyle, NUMBER_LIBRARY, NoteTag, Toggle, | ||
| 1473 | }; | ||
| 1474 | use commands::{Choice, Id as Cmd}; | 1484 | use commands::{Choice, Id as Cmd}; |
| 1475 | use ui::shell::TOOL; | 1485 | use ui::shell::TOOL; |
| 1476 | // A focused picture has no text to show a format for. | 1486 | // A focused picture has no text to show a format for. |
| 1477 | let state = self.format_state(); | 1487 | let state = self.format_state(); |
| 1478 | let statuses: Vec<_> = commands::COMMANDS | 1488 | let statuses: Vec<_> = commands::COMMANDS |
| 1479 | .iter() | 1489 | .iter() |
| 1480 | .map(|command| { | 1490 | .map(|command| command.id) |
| 1481 | ( | 1491 | .chain((0..self.tags.len()).map(Cmd::Tag)) |
| 1482 | command.id, | 1492 | .map(|id| (id, self.status(&Choice::Command(id), &state))) |
| 1483 | self.status(&Choice::Command(command.id), &state), | ||
| 1484 | ) | ||
| 1485 | }) | ||
| 1486 | .collect(); | 1493 | .collect(); |
| 1487 | let status_of = |id| { | 1494 | let status_of = |id| { |
| 1488 | statuses | 1495 | statuses |
| ... | @@ -1499,7 +1506,7 @@ impl State { | ... | @@ -1499,7 +1506,7 @@ impl State { |
| 1499 | let label = format!("{:.0}%", self.view.zoom() * 100.0); | 1506 | let label = format!("{:.0}%", self.view.zoom() * 100.0); |
| 1500 | let ui = &mut self.ui; | 1507 | let ui = &mut self.ui; |
| 1501 | let text = theme.text; | 1508 | let text = theme.text; |
| 1502 | let tags = NoteTag::ALL.map(Cmd::Tag); | 1509 | let tags: Vec<_> = self.tags.iter().enumerate().collect(); |
| 1503 | let mut choice = group( | 1510 | let mut choice = group( |
| 1504 | ui, | 1511 | ui, |
| 1505 | "navigate", | 1512 | "navigate", |
| ... | @@ -1890,11 +1897,13 @@ impl State { | ... | @@ -1890,11 +1897,13 @@ impl State { |
| 1890 | ) | 1897 | ) |
| 1891 | .or(choice); | 1898 | .or(choice); |
| 1892 | // The gallery's menu: every tag, then managing them. | 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 | all_tags.extend([ | 1904 | all_tags.extend([ |
| 1895 | Rule, | 1905 | Rule, |
| 1896 | // Customize Tags awaits #8. | 1906 | Run(Cmd::CustomizeTags), |
| 1897 | Soon("Customize Tags…", None), | ||
| 1898 | Run(Cmd::RemoveTags), | 1907 | Run(Cmd::RemoveTags), |
| 1899 | Rule, | 1908 | Rule, |
| 1900 | Run(Cmd::FindTags), | 1909 | Run(Cmd::FindTags), |
| ... | @@ -1906,12 +1915,18 @@ impl State { | ... | @@ -1906,12 +1915,18 @@ impl State { |
| 1906 | |ui| { | 1915 | |ui| { |
| 1907 | divider(ui, theme); | 1916 | divider(ui, theme); |
| 1908 | let mut choice = None; | 1917 | let mut choice = None; |
| 1909 | for id in &tags[..3] { | 1918 | for &(place, tag) in tags.iter().take(3) { |
| 1910 | choice = tool(ui, *id, status_of(*id)).or(choice); | 1919 | choice = tag_tool(ui, place, tag, status_of(Cmd::Tag(place))).or(choice); |
| 1911 | } | 1920 | } |
| 1912 | dropdown(ui, "tags", Head::More, &all_tags, status_of).or(choice) | 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 | .or(choice); | 1931 | .or(choice); |
| 1917 | let inserted = [Cmd::Picture, Cmd::Link, Cmd::Date, Cmd::Equation]; | 1932 | let inserted = [Cmd::Picture, Cmd::Link, Cmd::Date, Cmd::Equation]; |
| ... | @@ -2342,6 +2357,7 @@ impl State { | ... | @@ -2342,6 +2357,7 @@ impl State { |
| 2342 | self.respond(response); | 2357 | self.respond(response); |
| 2343 | } | 2358 | } |
| 2344 | Command::Page(Request::OpenLink(address)) => self.open_link(&address)?, | 2359 | Command::Page(Request::OpenLink(address)) => self.open_link(&address)?, |
| 2360 | Command::Page(Request::OpenAttachment(file)) => self.open_attachment(&file)?, | ||
| 2345 | Command::Choose(choice) => self.run(choice)?, | 2361 | Command::Choose(choice) => self.run(choice)?, |
| 2346 | } | 2362 | } |
| 2347 | Ok(()) | 2363 | Ok(()) |
| ... | @@ -3789,8 +3805,10 @@ fn group( | ... | @@ -3789,8 +3805,10 @@ fn group( |
| 3789 | 3805 | ||
| 3790 | /// A row of a toolbar menu. | 3806 | /// A row of a toolbar menu. |
| 3791 | #[derive(Clone, Copy)] | 3807 | #[derive(Clone, Copy)] |
| 3792 | enum Entry { | 3808 | enum Entry<'a> { |
| 3793 | Run(commands::Id), | 3809 | Run(commands::Id), |
| 3810 | /// Applies the tag at this place in the tag list. | ||
| 3811 | Tag(usize, &'a canvas::editor::NoteTag), | ||
| 3794 | /// Opens the toolbar's popup `name`, which applies the command, under the button the | 3812 | /// Opens the toolbar's popup `name`, which applies the command, under the button the |
| 3795 | /// popup's own control would open it from, or where that is folded away, this menu's. | 3813 | /// popup's own control would open it from, or where that is folded away, this menu's. |
| 3796 | Open(&'static str, commands::Id), | 3814 | Open(&'static str, commands::Id), |
| ... | @@ -3802,8 +3820,10 @@ enum Entry { | ... | @@ -3802,8 +3820,10 @@ enum Entry { |
| 3802 | 3820 | ||
| 3803 | /// What opens a toolbar menu: the arrow of a button running a command, a button showing | 3821 | /// What opens a toolbar menu: the arrow of a button running a command, a button showing |
| 3804 | /// artwork, or a bare arrow. | 3822 | /// artwork, or a bare arrow. |
| 3805 | enum Head { | 3823 | enum Head<'a> { |
| 3806 | Split(commands::Id), | 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 | Menu(&'static [&'static str]), | 3827 | Menu(&'static [&'static str]), |
| 3808 | More, | 3828 | More, |
| 3809 | } | 3829 | } |
| ... | @@ -3831,8 +3851,17 @@ fn dropdown( | ... | @@ -3831,8 +3851,17 @@ fn dropdown( |
| 3831 | tip(ui, id); | 3851 | tip(ui, id); |
| 3832 | ui::Anchor::Below(ui.rect(button).unwrap_or_default()) | 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 | // Where its command does not apply, the button only opens the menu. | 3862 | // Where its command does not apply, the button only opens the menu. |
| 3835 | Head::Split(id) => ui::shell::menu_button(ui, part, artwork(id).unwrap_or_default(), menu), | 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 | Head::Menu(icon) => ui::shell::menu_button(ui, part, icon, menu), | 3865 | Head::Menu(icon) => ui::shell::menu_button(ui, part, icon, menu), |
| 3837 | Head::More => { | 3866 | Head::More => { |
| 3838 | let open = ui.popup_open(menu); | 3867 | let open = ui.popup_open(menu); |
| ... | @@ -3847,6 +3876,7 @@ fn dropdown( | ... | @@ -3847,6 +3876,7 @@ fn dropdown( |
| 3847 | .iter() | 3876 | .iter() |
| 3848 | .map(|entry| match entry { | 3877 | .map(|entry| match entry { |
| 3849 | Entry::Run(id) => commands::shortcut(*id), | 3878 | Entry::Run(id) => commands::shortcut(*id), |
| 3879 | Entry::Tag(place, _) => commands::shortcut(commands::Id::Tag(*place)), | ||
| 3850 | _ => String::new(), | 3880 | _ => String::new(), |
| 3851 | }) | 3881 | }) |
| 3852 | .collect(); | 3882 | .collect(); |
| ... | @@ -3861,12 +3891,22 @@ fn dropdown( | ... | @@ -3861,12 +3891,22 @@ fn dropdown( |
| 3861 | Entry::Run(id) => ui::popup::Item { | 3891 | Entry::Run(id) => ui::popup::Item { |
| 3862 | text: commands::command(id).title, | 3892 | text: commands::command(id).title, |
| 3863 | icon: artwork(id), | 3893 | icon: artwork(id), |
| 3864 | colored: matches!(id, commands::Id::Tag(_)), | ||
| 3865 | shortcut: key, | 3894 | shortcut: key, |
| 3866 | checked: status_of(id).checked, | 3895 | checked: status_of(id).checked, |
| 3867 | disabled: !status_of(id).enabled, | 3896 | disabled: !status_of(id).enabled, |
| 3868 | ..Default::default() | 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 | Entry::Open(_, id) => ui::popup::Item { | 3910 | Entry::Open(_, id) => ui::popup::Item { |
| 3871 | text: commands::command(id).title, | 3911 | text: commands::command(id).title, |
| 3872 | icon: artwork(id), | 3912 | icon: artwork(id), |
| ... | @@ -3889,6 +3929,7 @@ fn dropdown( | ... | @@ -3889,6 +3929,7 @@ fn dropdown( |
| 3889 | } | 3929 | } |
| 3890 | match ui::popup::menu(ui, menu, anchor, &items, None).map(|index| actions[index]) { | 3930 | match ui::popup::menu(ui, menu, anchor, &items, None).map(|index| actions[index]) { |
| 3891 | Some(Entry::Run(id)) => Some(Choice::Command(id)), | 3931 | Some(Entry::Run(id)) => Some(Choice::Command(id)), |
| 3932 | Some(Entry::Tag(place, _)) => Some(Choice::Command(commands::Id::Tag(place))), | ||
| 3892 | Some(Entry::Open(name, _)) => { | 3933 | Some(Entry::Open(name, _)) => { |
| 3893 | ui.open_popup(toolbar_popup(name)); | 3934 | ui.open_popup(toolbar_popup(name)); |
| 3894 | None | 3935 | None |
| ... | @@ -3904,12 +3945,7 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma | ... | @@ -3904,12 +3945,7 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma |
| 3904 | .iter() | 3945 | .iter() |
| 3905 | .position(|command| command.id == id); | 3946 | .position(|command| command.id == id); |
| 3906 | let icon = artwork(id).unwrap_or_default(); | 3947 | let icon = artwork(id).unwrap_or_default(); |
| 3907 | // Artwork in its own colours takes white. | 3948 | let tint = ui.theme.text; |
| 3908 | let tint = if matches!(id, commands::Id::Tag(_)) { | ||
| 3909 | [1.0; 4] | ||
| 3910 | } else { | ||
| 3911 | ui.theme.text | ||
| 3912 | }; | ||
| 3913 | if !status.enabled { | 3949 | if !status.enabled { |
| 3914 | ui::shell::unavailable(ui, part, icon, tint, false); | 3950 | ui::shell::unavailable(ui, part, icon, tint, false); |
| 3915 | return None; | 3951 | return None; |
| ... | @@ -3919,6 +3955,25 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma | ... | @@ -3919,6 +3955,25 @@ fn tool(ui: &mut Ui, id: commands::Id, status: commands::Status) -> Option<comma |
| 3919 | clicked.then_some(commands::Choice::Command(id)) | 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 | /// The artwork the toolbar and its menus show for command `id`. | 3977 | /// The artwork the toolbar and its menus show for command `id`. |
| 3923 | fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { | 3978 | fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { |
| 3924 | use canvas::editor::{Alignment, Toggle}; | 3979 | use canvas::editor::{Alignment, Toggle}; |
| ... | @@ -3948,7 +4003,6 @@ fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { | ... | @@ -3948,7 +4003,6 @@ fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { |
| 3948 | Cmd::Align(Alignment::Right) => art::ALIGN_RIGHT, | 4003 | Cmd::Align(Alignment::Right) => art::ALIGN_RIGHT, |
| 3949 | Cmd::Outdent => art::OUTDENT, | 4004 | Cmd::Outdent => art::OUTDENT, |
| 3950 | Cmd::Indent => art::INDENT, | 4005 | Cmd::Indent => art::INDENT, |
| 3951 | Cmd::Tag(tag) => tag_icon(tag), | ||
| 3952 | Cmd::FindTags => art::FIND_TAGS, | 4006 | Cmd::FindTags => art::FIND_TAGS, |
| 3953 | Cmd::Table => art::TABLE, | 4007 | Cmd::Table => art::TABLE, |
| 3954 | Cmd::Picture => art::PICTURE, | 4008 | Cmd::Picture => art::PICTURE, |
| ... | @@ -3970,14 +4024,36 @@ fn artwork(id: commands::Id) -> Option<&'static [&'static str]> { | ... | @@ -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`. | 4027 | /// The artwork the toolbar shows for `tag`: its symbol, or for a tag that only colours |
| 3974 | fn tag_icon(tag: canvas::editor::NoteTag) -> &'static [&'static str] { | 4028 | /// text, a chip of lines in its font colour on its highlight. |
| 3975 | use canvas::editor::NoteTag; | 4029 | fn tag_art(tag: &canvas::editor::NoteTag) -> &'static [&'static str] { |
| 3976 | match tag { | 4030 | use std::collections::BTreeMap; |
| 3977 | NoteTag::RememberForLater => art::TAG_REMEMBER, | 4031 | use std::sync::Mutex; |
| 3978 | NoteTag::Definition => art::TAG_DEFINITION, | 4032 | // Artwork is borrowed for the program's life, so each colour pair is made once. |
| 3979 | tag => tag_sources(TagIcon::of(tag.shape(), false).unwrap_or(TagIcon::Other)), | 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 | /// The toolbar's popup `name`, which the menu bar opens too. | 4059 | /// The toolbar's popup `name`, which the menu bar opens too. |
| ... | @@ -3985,6 +4061,12 @@ fn toolbar_popup(name: &str) -> Id { | ... | @@ -3985,6 +4061,12 @@ fn toolbar_popup(name: &str) -> Id { |
| 3985 | Id::ROOT.child(("popup", name)) | 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 | /// A tooltip naming command `id` and its chord on the box built last. It names the command, | 4070 | /// A tooltip naming command `id` and its chord on the box built last. It names the command, |
| 3989 | /// not a dialog it opens, so it leaves off the menu's ellipsis. | 4071 | /// not a dialog it opens, so it leaves off the menu's ellipsis. |
| 3990 | fn tip(ui: &mut Ui, id: commands::Id) { | 4072 | fn tip(ui: &mut Ui, id: commands::Id) { |
| ... | @@ -4344,6 +4426,11 @@ impl ApplicationHandler<UserEvent> for App { | ... | @@ -4344,6 +4426,11 @@ impl ApplicationHandler<UserEvent> for App { |
| 4344 | }) | 4426 | }) |
| 4345 | } | 4427 | } |
| 4346 | WindowEvent::Ime(ime) => state.input(ui::Event::Ime(ime)), | 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 | WindowEvent::PinchGesture { delta, .. } => state.pinch(1.0 + delta as f32)?, | 4434 | WindowEvent::PinchGesture { delta, .. } => state.pinch(1.0 + delta as f32)?, |
| 4348 | // Smart zoom: to 200% about the fingers, or back to 100%. | 4435 | // Smart zoom: to 200% about the fingers, or back to 100%. |
| 4349 | WindowEvent::DoubleTapGesture { .. } => { | 4436 | WindowEvent::DoubleTapGesture { .. } => { |
crates/snowbound/src/manage.rs+81-68| ... | @@ -7,7 +7,7 @@ use notebook::session::Notebook; | ... | @@ -7,7 +7,7 @@ use notebook::session::Notebook; |
| 7 | use onestore::{ | 7 | use onestore::{ |
| 8 | ExGuid, PageCreation, PageEdit, | 8 | ExGuid, PageCreation, PageEdit, |
| 9 | op::{Edit, Op, PageOp, SectionOp}, | 9 | op::{Edit, Op, PageOp, SectionOp}, |
| 10 | page::{Image, Page, PageObject}, | 10 | page::{Page, PageObject}, |
| 11 | }; | 11 | }; |
| 12 | use std::{error::Error, sync::Arc}; | 12 | use std::{error::Error, sync::Arc}; |
| 13 | 13 | ||
| ... | @@ -134,26 +134,19 @@ impl State { | ... | @@ -134,26 +134,19 @@ impl State { |
| 134 | self.edit_page(ops) | 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 | 137 | /// View, Page Color and Rule Lines: gives the open page `color`, a COLORREF, |
| 138 | /// and rule lines stay. | 138 | /// `rule_lines` and, when given, `template`'s art or none in place of the art it had, as |
| 139 | pub(crate) fn art_page( | 139 | /// one undo step. |
| 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. | ||
| 151 | pub(crate) fn paper_page( | 140 | pub(crate) fn paper_page( |
| 152 | &mut self, | 141 | &mut self, |
| 153 | color: Option<u32>, | 142 | color: Option<u32>, |
| 154 | rule_lines: Option<onestore::page::RuleLines>, | 143 | rule_lines: Option<onestore::page::RuleLines>, |
| 144 | art: Option<Option<&Template>>, | ||
| 155 | ) -> Result<(), Box<dyn Error>> { | 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 | self.respond(response); | 150 | self.respond(response); |
| 158 | Ok(()) | 151 | Ok(()) |
| 159 | } | 152 | } |
| ... | @@ -177,7 +170,7 @@ impl State { | ... | @@ -177,7 +170,7 @@ impl State { |
| 177 | 170 | ||
| 178 | /// Asks where to create a notebook and creates it, as File, New does in OneNote. | 171 | /// Asks where to create a notebook and creates it, as File, New does in OneNote. |
| 179 | pub(crate) fn new_notebook(&mut self) -> Result<(), Box<dyn Error>> { | 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 | return Ok(()); | 174 | return Ok(()); |
| 182 | }; | 175 | }; |
| 183 | let page = self.dated_page(None)?; | 176 | let page = self.dated_page(None)?; |
| ... | @@ -503,25 +496,9 @@ pub fn art_ops(page: &Page, template: Option<&Template>) -> Result<Vec<PageOp>, | ... | @@ -503,25 +496,9 @@ pub fn art_ops(page: &Page, template: Option<&Template>) -> Result<Vec<PageOp>, |
| 503 | _ => true, | 496 | _ => true, |
| 504 | }) | 497 | }) |
| 505 | .map(PageObject::id); | 498 | .map(PageObject::id); |
| 506 | for art in template.map_or(&[][..], |template| template.art) { | 499 | for image in template.map_or(Ok(Vec::new()), Template::pictures)? { |
| 507 | let bytes = | ||
| 508 | canvas::gpu::page::template_picture(art.art).ok_or("That template's art is missing")?; | ||
| 509 | ops.push(PageOp::Add { | 500 | ops.push(PageOp::Add { |
| 510 | object: PageObject::Image(Image { | 501 | 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 | }), | ||
| 525 | before: under, | 502 | before: under, |
| 526 | }); | 503 | }); |
| 527 | } | 504 | } |
| ... | @@ -873,9 +850,10 @@ mod tests { | ... | @@ -873,9 +850,10 @@ mod tests { |
| 873 | } | 850 | } |
| 874 | 851 | ||
| 875 | /// Backgrounds the Page Color menu gives OneNote's ruled pages | 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 | 853 | /// (`corpus/rule-lines/native`) through the editor, each step one edit: art on a page |
| 877 | /// away, and a colour outside OneNote's menu. `SNOWBOUND_BACKGROUND_EXPORT` names a new | 854 | /// with rule lines, keeping them; art taken away, undone, redone and replaced; a colour |
| 878 | /// directory that receives the notebook for a cold reopen in OneNote 2010. | 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 | #[test] | 857 | #[test] |
| 880 | fn backgrounds_go_on_and_off_pages_that_have_content() { | 858 | fn backgrounds_go_on_and_off_pages_that_have_content() { |
| 881 | let temporary = | 859 | let temporary = |
| ... | @@ -896,38 +874,73 @@ mod tests { | ... | @@ -896,38 +874,73 @@ mod tests { |
| 896 | .unwrap() | 874 | .unwrap() |
| 897 | .0 | 875 | .0 |
| 898 | }; | 876 | }; |
| 899 | let on_page = |space, ops: Vec<PageOp>| { | 877 | enum Step { |
| 900 | edit( | 878 | Art(Option<&'static str>), |
| 901 | &file, | 879 | Color(u32), |
| 902 | ops.into_iter().map(|op| Op::Page { space, op }).collect(), | 880 | Undo, |
| 903 | ) | 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"); | 912 | let lemon = canvas::template::PAGE_COLORS[2]; |
| 906 | let (standard, wide, grid) = (titled("Standard"), titled("Wide"), titled("SmallGrid")); | 913 | assert_eq!(lemon.0, "Lemon"); |
| 907 | on_page(standard, art_ops(&page(&file, standard), ivy).unwrap()); | 914 | let pages = [ |
| 908 | on_page(wide, art_ops(&page(&file, wide), ivy).unwrap()); | 915 | run("Standard", &[Art(Some("Ivy"))]), |
| 909 | on_page(wide, art_ops(&page(&file, wide), None).unwrap()); | 916 | run("Wide", &[Art(Some("Ivy")), Art(None)]), |
| 910 | // A warm grey no menu offers. | 917 | // A warm grey no menu offers. |
| 911 | on_page(grid, vec![PageOp::Color(Some(0x00c8d8e8))]); | 918 | run("SmallGrid", &[Color(0x00c8d8e8)]), |
| 912 | 919 | run("College", &[Art(Some("Ivy")), Undo]), | |
| 913 | let standard = page(&file, standard); | 920 | run("MediumGrid", &[Art(Some("Ivy")), Undo, Redo]), |
| 914 | assert!(standard.rule_lines.is_some()); | 921 | run("LargeGrid", &[Art(Some("Ivy")), Art(Some("Tulips")), Undo]), |
| 915 | assert_eq!( | 922 | run("VeryLargeGrid", &[Color(lemon.1), Art(Some("Sparks"))]), |
| 916 | standard | 923 | ]; |
| 917 | .objects | 924 | let art = |page: &Page| -> Vec<f32> { |
| 925 | page.objects | ||
| 918 | .iter() | 926 | .iter() |
| 919 | .filter(|object| matches!(object, PageObject::Image(image) if image.background)) | 927 | .filter_map(|object| match object { |
| 920 | .count(), | 928 | PageObject::Image(image) if image.background => image.layout.max_width, |
| 921 | ivy.unwrap().art.len() | 929 | _ => None, |
| 922 | ); | 930 | }) |
| 923 | let wide = page(&file, wide); | 931 | .collect() |
| 924 | assert!( | 932 | }; |
| 925 | !wide | 933 | let [standard, wide, grid, college, medium, large, very_large] = &pages; |
| 926 | .objects | 934 | let [ivy, sparks] = ["Ivy", "Sparks"] |
| 927 | .iter() | 935 | .map(|name| canvas::template::find(name).unwrap().art[0].size.unwrap()[0]); |
| 928 | .any(|object| matches!(object, PageObject::Image(_))) | 936 | assert_eq!(art(standard), [ivy]); |
| 929 | ); | 937 | assert!(pages.iter().all(|page| page.rule_lines.is_some())); |
| 930 | assert_eq!(page(&file, grid).color, Some(0x00c8d8e8)); | 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 | if let Some(directory) = std::env::var_os("SNOWBOUND_BACKGROUND_EXPORT") { | 944 | if let Some(directory) = std::env::var_os("SNOWBOUND_BACKGROUND_EXPORT") { |
| 932 | copy(&temporary, Path::new(&directory)); | 945 | copy(&temporary, Path::new(&directory)); |
| 933 | } | 946 | } |
crates/snowbound/src/menubar.rs+37-42| ... | @@ -156,48 +156,8 @@ const MENUS: &[Item] = &[ | ... | @@ -156,48 +156,8 @@ const MENUS: &[Item] = &[ |
| 156 | C(Id::ClearFormatting), | 156 | C(Id::ClearFormatting), |
| 157 | ], | 157 | ], |
| 158 | ), | 158 | ), |
| 159 | Item::Menu( | 159 | // Filled from the tag list by `tags`. |
| 160 | "Tags", | 160 | Item::Menu("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 | ), | ||
| 201 | Item::Menu( | 161 | Item::Menu( |
| 202 | "Window", | 162 | "Window", |
| 203 | &[ | 163 | &[ |
| ... | @@ -216,6 +176,7 @@ pub struct Menus { | ... | @@ -216,6 +176,7 @@ pub struct Menus { |
| 216 | pub services: Retained<NSMenu>, | 176 | pub services: Retained<NSMenu>, |
| 217 | pub window: Retained<NSMenu>, | 177 | pub window: Retained<NSMenu>, |
| 218 | pub help: Retained<NSMenu>, | 178 | pub help: Retained<NSMenu>, |
| 179 | pub tags: Retained<NSMenu>, | ||
| 219 | } | 180 | } |
| 220 | 181 | ||
| 221 | /// Builds the bar; the table's items send `choose:` to `target`, which validates them. | 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,6 +202,7 @@ pub fn build(mtm: MainThreadMarker, target: &AnyObject) -> Menus { |
| 241 | .expect("The bar has the menus AppKit fills") | 202 | .expect("The bar has the menus AppKit fills") |
| 242 | }; | 203 | }; |
| 243 | Menus { | 204 | Menus { |
| 205 | tags: named("Tags"), | ||
| 244 | window: named("Window"), | 206 | window: named("Window"), |
| 245 | help: named("Help"), | 207 | help: named("Help"), |
| 246 | services, | 208 | services, |
| ... | @@ -302,6 +264,38 @@ fn menu( | ... | @@ -302,6 +264,38 @@ fn menu( |
| 302 | menu | 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 | fn entry( | 299 | fn entry( |
| 306 | mtm: MainThreadMarker, | 300 | mtm: MainThreadMarker, |
| 307 | title: &str, | 301 | title: &str, |
| ... | @@ -418,6 +412,7 @@ mod tests { | ... | @@ -418,6 +412,7 @@ mod tests { |
| 418 | let target: Retained<AnyObject> = | 412 | let target: Retained<AnyObject> = |
| 419 | unsafe { objc2::msg_send_id![objc2::class!(NSObject), new] }; | 413 | unsafe { objc2::msg_send_id![objc2::class!(NSObject), new] }; |
| 420 | let menus = build(mtm, &target); | 414 | let menus = build(mtm, &target); |
| 415 | tags(mtm, &target, &menus.tags, &NoteTag::defaults()); | ||
| 421 | let (mut tree, mut chords) = (String::new(), Vec::new()); | 416 | let (mut tree, mut chords) = (String::new(), Vec::new()); |
| 422 | dump(&menus.bar, 0, &mut tree, &mut chords); | 417 | dump(&menus.bar, 0, &mut tree, &mut chords); |
| 423 | println!("{tree}"); | 418 | println!("{tree}"); |
crates/snowbound/src/palette.rs+9| ... | @@ -44,6 +44,15 @@ impl State { | ... | @@ -44,6 +44,15 @@ impl State { |
| 44 | target: Some(Target::Command(command.id)), | 44 | target: Some(Target::Command(command.id)), |
| 45 | }) | 45 | }) |
| 46 | .collect(); | 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 | let heading = |text: &str| Row { | 56 | let heading = |text: &str| Row { |
| 48 | text: text.to_owned(), | 57 | text: text.to_owned(), |
| 49 | after: String::new(), | 58 | after: String::new(), |
crates/snowbound/src/settings.rs+9| ... | @@ -24,6 +24,8 @@ pub struct Settings { | ... | @@ -24,6 +24,8 @@ pub struct Settings { |
| 24 | pub light_pages: bool, | 24 | pub light_pages: bool, |
| 25 | /// Where searches look first, as "Set This Scope as Default" chose. | 25 | /// Where searches look first, as "Set This Scope as Default" chose. |
| 26 | pub search_scope: crate::search::Scope, | 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 | /// What the toolbar's buttons apply from their menus' last picks. | 31 | /// What the toolbar's buttons apply from their menus' last picks. |
| ... | @@ -128,6 +130,7 @@ impl crate::State { | ... | @@ -128,6 +130,7 @@ impl crate::State { |
| 128 | color_scheme: self.color_scheme, | 130 | color_scheme: self.color_scheme, |
| 129 | light_pages: self.light_pages, | 131 | light_pages: self.light_pages, |
| 130 | search_scope: self.search.default, | 132 | search_scope: self.search.default, |
| 133 | tags: (self.tags != canvas::editor::NoteTag::defaults()).then(|| self.tags.clone()), | ||
| 131 | }; | 134 | }; |
| 132 | if let Err(error) = settings.save(path) { | 135 | if let Err(error) = settings.save(path) { |
| 133 | eprintln!("Cannot save the settings in {}: {error}", path.display()); | 136 | eprintln!("Cannot save the settings in {}: {error}", path.display()); |
| ... | @@ -177,6 +180,12 @@ mod tests { | ... | @@ -177,6 +180,12 @@ mod tests { |
| 177 | color_scheme: ColorScheme::Dark, | 180 | color_scheme: ColorScheme::Dark, |
| 178 | light_pages: true, | 181 | light_pages: true, |
| 179 | search_scope: crate::search::Scope::Notebook, | 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 | settings.save(&path).unwrap(); | 190 | settings.save(&path).unwrap(); |
| 182 | assert_eq!(Settings::load(&path), settings); | 191 | assert_eq!(Settings::load(&path), settings); |
crates/snowbound/src/sync.rs+10-29| ... | @@ -2,8 +2,8 @@ | ... | @@ -2,8 +2,8 @@ |
| 2 | //! Synchronization for the open section's notebook and each of its sections. | 2 | //! Synchronization for the open section's notebook and each of its sections. |
| 3 | 3 | ||
| 4 | use crate::{Session, State, art, filetime, library, platform}; | 4 | use crate::{Session, State, art, filetime, library, platform}; |
| 5 | use notebook::session::SyncStatus; | 5 | use notebook::session::{SyncState, SyncStatus}; |
| 6 | use std::{error::Error, io::ErrorKind}; | 6 | use std::error::Error; |
| 7 | use ui::{ | 7 | use ui::{ |
| 8 | Anchor, Axis, Flags, Id, Overflow, Spec, Theme, Ui, children, fill, fit, px, shell::TOOL, | 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,7 +20,6 @@ fn button() -> Id { |
| 20 | 20 | ||
| 21 | /// The status's label and icon, and what the reader can do about an error. | 21 | /// The status's label and icon, and what the reader can do about an error. |
| 22 | fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option<&'static str>) { | 22 | fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option<&'static str>) { |
| 23 | use ErrorKind::*; | ||
| 24 | if library::offline() { | 23 | if library::offline() { |
| 25 | return ( | 24 | return ( |
| 26 | "Working offline", | 25 | "Working offline", |
| ... | @@ -28,48 +27,30 @@ fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option | ... | @@ -28,48 +27,30 @@ fn describe(sync: &SyncStatus) -> (&'static str, &'static [&'static str], Option |
| 28 | Some("Changes stay on this computer until you sync."), | 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) { | 30 | match sync.state() { |
| 32 | Some(PermissionDenied | ReadOnlyFilesystem) => ( | 31 | SyncState::ReadOnly => ( |
| 33 | "Unable to sync", | 32 | "Unable to sync", |
| 34 | art::SYNC_ERROR, | 33 | art::SYNC_ERROR, |
| 35 | Some( | 34 | Some( |
| 36 | "You can’t change this notebook where it’s stored. Changes stay on this computer.", | 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 | "Section in use", | 39 | "Section in use", |
| 41 | art::SYNC_BUSY, | 40 | art::SYNC_BUSY, |
| 42 | Some("Someone else is saving this section. Sync continues when they finish."), | 41 | Some("Someone else is saving this section. Sync continues when they finish."), |
| 43 | ), | 42 | ), |
| 44 | Some( | 43 | SyncState::NotConnected => ( |
| 45 | NotFound | ConnectionRefused | ConnectionReset | ConnectionAborted | NotConnected | ||
| 46 | | TimedOut | HostUnreachable | NetworkUnreachable | NetworkDown | BrokenPipe | ||
| 47 | | AddrNotAvailable, | ||
| 48 | ) => ( | ||
| 49 | "Not connected", | 44 | "Not connected", |
| 50 | art::SYNC_OFFLINE, | 45 | art::SYNC_OFFLINE, |
| 51 | Some("Changes stay on this computer and sync when the notebook is back."), | 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), | 48 | SyncState::Failed => ("Unable to sync", art::SYNC_ERROR, None), |
| 54 | None if sync.synced.is_none() || sync.queued > 0 => ("Syncing…", art::SYNC_BUSY, None), | 49 | SyncState::Syncing => ("Syncing…", art::SYNC_BUSY, None), |
| 55 | None => ("Up to date", art::SYNC_DONE, 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 | /// Every section of the open section's notebook with its status, in catalog order: the | 54 | /// Every section of the open section's notebook with its status, in catalog order: the |
| 74 | /// open one's from its session, the others' from the notebook's background sync. | 55 | /// open one's from its session, the others' from the notebook's background sync. |
| 75 | fn sections(session: &Session) -> Vec<(String, SyncStatus)> { | 56 | fn sections(session: &Session) -> Vec<(String, SyncStatus)> { |
| ... | @@ -102,7 +83,7 @@ fn overall(sections: &[(String, SyncStatus)]) -> SyncStatus { | ... | @@ -102,7 +83,7 @@ fn overall(sections: &[(String, SyncStatus)]) -> SyncStatus { |
| 102 | .iter() | 83 | .iter() |
| 103 | .map(|(_, sync)| sync) | 84 | .map(|(_, sync)| sync) |
| 104 | .filter(|sync| sync.error.is_some()) | 85 | .filter(|sync| sync.error.is_some()) |
| 105 | .max_by_key(|sync| rank(sync)); | 86 | .max_by_key(|sync| sync.state()); |
| 106 | SyncStatus { | 87 | SyncStatus { |
| 107 | synced: sections | 88 | synced: sections |
| 108 | .iter() | 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,6 +606,12 @@ enum Display { |
| 606 | /// Top and bottom. | 606 | /// Top and bottom. |
| 607 | colors: [[f32; 4]; 2], | 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 | Icon { | 615 | Icon { |
| 610 | sources: &'static [&'static str], | 616 | sources: &'static [&'static str], |
| 611 | origin: [f32; 2], | 617 | origin: [f32; 2], |
| ... | @@ -1452,12 +1458,26 @@ impl Ui { | ... | @@ -1452,12 +1458,26 @@ impl Ui { |
| 1452 | spread, | 1458 | spread, |
| 1453 | } in shadows | 1459 | } in shadows |
| 1454 | { | 1460 | { |
| 1461 | let origin = [painted[0] - spread, painted[1] + drop - spread]; | ||
| 1455 | let grown = size.map(|side| side + 2.0 * spread); | 1462 | let grown = size.map(|side| side + 2.0 * spread); |
| 1456 | self.display.push(Display::Path { | 1463 | self.display.push(match node.shape { |
| 1457 | data: outline(node.shape, grown, node.radius + spread), | 1464 | Shape::Rounded => Display::Shadow { |
| 1458 | origin: [painted[0] - spread, painted[1] + drop - spread], | 1465 | rect: [ |
| 1459 | style: PathStyle::Shadow(blur), | 1466 | origin[0], |
| 1460 | colors: [color; 2], | 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,6 +1743,17 @@ impl Ui { |
| 1723 | style: *style, | 1743 | style: *style, |
| 1724 | colors: *colors, | 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 | Display::Segment { from, to, color } => primitives.push(Primitive::Segment { | 1757 | Display::Segment { from, to, color } => primitives.push(Primitive::Segment { |
| 1727 | from: *from, | 1758 | from: *from, |
| 1728 | to: *to, | 1759 | to: *to, |
crates/ui/src/popup.rs+57-13| ... | @@ -27,6 +27,8 @@ const PALETTE: f32 = 560.0; | ... | @@ -27,6 +27,8 @@ const PALETTE: f32 = 560.0; |
| 27 | const TIP_WIDTH: f32 = 280.0; | 27 | const TIP_WIDTH: f32 = 280.0; |
| 28 | /// A colour grid's cell, around its swatch. | 28 | /// A colour grid's cell, around its swatch. |
| 29 | const CELL: f32 = 22.0; | 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 | /// Seconds a popup takes to ease to the height of its results. | 32 | /// Seconds a popup takes to ease to the height of its results. |
| 31 | const RESIZE: f32 = 0.15; | 33 | const RESIZE: f32 = 0.15; |
| 32 | thread_local! { | 34 | thread_local! { |
| ... | @@ -50,6 +52,10 @@ pub struct Item<'a> { | ... | @@ -50,6 +52,10 @@ pub struct Item<'a> { |
| 50 | pub text: &'a str, | 52 | pub text: &'a str, |
| 51 | /// The family the text previews, as in a font menu. | 53 | /// The family the text previews, as in a font menu. |
| 52 | pub font: Option<&'a str>, | 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 | pub icon: Option<&'static [&'static str]>, | 59 | pub icon: Option<&'static [&'static str]>, |
| 54 | /// The icon has colours of its own, so the text's colour does not tint it. | 60 | /// The icon has colours of its own, so the text's colour does not tint it. |
| 55 | pub colored: bool, | 61 | pub colored: bool, |
| ... | @@ -87,7 +93,14 @@ pub fn menu( | ... | @@ -87,7 +93,14 @@ pub fn menu( |
| 87 | }; | 93 | }; |
| 88 | let [text, shortcut] = items.iter().fold([0.0_f32; 2], |[text, shortcut], item| { | 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 | shortcut.max(measure(item.shortcut)), | 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,18 +427,49 @@ fn menu_row(ui: &mut Ui, style: &Menu, matches: &Matches, row: Row) { |
| 414 | }, | 427 | }, |
| 415 | ); | 428 | ); |
| 416 | } | 429 | } |
| 417 | ui.leaf( | 430 | let text = Spec { |
| 418 | "text", | 431 | size: [fill(), fill()], |
| 419 | Spec { | 432 | text: Some(item.text), |
| 420 | size: [fill(), fill()], | 433 | font: item.font, |
| 421 | text: Some(item.text), | 434 | font_size: Some(style.font_size - if item.heading { 2.0 } else { 0.0 }), |
| 422 | font: item.font, | 435 | bold: item.heading, |
| 423 | font_size: Some(style.font_size - if item.heading { 2.0 } else { 0.0 }), | 436 | color: Some(match (item.ink, item.highlight) { |
| 424 | bold: item.heading, | 437 | (None, None) => color, |
| 425 | color: Some(color), | 438 | // Automatic text on a highlight is black, as on OneNote's page. |
| 426 | ..Spec::default() | 439 | (ink, highlight) => { |
| 427 | }, | 440 | let ink = ink.unwrap_or([0.0, 0.0, 0.0, 1.0]); |
| 428 | ); | 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 | if !item.shortcut.is_empty() { | 473 | if !item.shortcut.is_empty() { |
| 430 | ui.leaf( | 474 | ui.leaf( |
| 431 | "shortcut", | 475 | "shortcut", |
tools/native/read.ps1+3-1| ... | @@ -99,7 +99,9 @@ try { | ... | @@ -99,7 +99,9 @@ try { |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | 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 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | 105 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); |
| 104 | sha256 = $hash; bytes = $bytes.Length } | 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,19 +24,29 @@ class PageBackgroundTest(unittest.TestCase): |
| 24 | pages[page.get('name')] = page | 24 | pages[page.get('name')] = page |
| 25 | 25 | ||
| 26 | def art(title): | 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 | def rules(title): | 30 | def rules(title): |
| 30 | return pages[title].find('one:PageSettings/one:RuleLines', ns).get('visible') | 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 | def color(title): |
| 33 | self.assertEqual(art('Standard'), ['true']) | 34 | return pages[title].find('one:PageSettings', ns).get('color') |
| 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') | ||
| 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 | if __name__ == '__main__': | 51 | if __name__ == '__main__': |
| 42 | unittest.main() | 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,7 +178,8 @@ def compare_objects(space, roots, page, native_roots, assets, native_payloads, a |
| 178 | frame_size = stored_size + (1.44 if kind['printout'] else 0) | 178 | frame_size = stored_size + (1.44 if kind['printout'] else 0) |
| 179 | assert abs(frame_size - native_size) < .002, ('Image display dimension differs', axis, frame_size, native_size) | 179 | assert abs(frame_size - native_size) < .002, ('Image display dimension differs', axis, frame_size, native_size) |
| 180 | else: | 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 | assert len(expected) == 1, 'Native attachment association is ambiguous' | 183 | assert len(expected) == 1, 'Native attachment association is ambiguous' |
| 183 | assert hashlib.sha256(data).hexdigest() == expected[0]['sha256'], 'Associated attachment bytes differ' | 184 | assert hashlib.sha256(data).hexdigest() == expected[0]['sha256'], 'Associated attachment bytes differ' |
| 184 | assert kind['filename'] == expected[0]['name'], 'Attachment filename differs' | 185 | assert kind['filename'] == expected[0]['name'], 'Attachment filename differs' |