diff --git a/Cargo.lock b/Cargo.lock
index 2f0ed2a7353046ddd92cc23e10675b351d8b3461..b80017633661a297d29e118e126a2a0c36832b22 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1888,6 +1888,7 @@ dependencies = [
"onestore",
"parley",
"pollster",
+ "resvg",
"serde",
"serde_json",
"tempfile",
diff --git a/apps/ios/Snowbound/App.swift b/apps/ios/Snowbound/App.swift
index 88845507d9930415dab3827ebcffc87e2a38aa6e..d536d95c80df1c3ac722916bb0dc81ff58a1c92e 100644
--- a/apps/ios/Snowbound/App.swift
+++ b/apps/ios/Snowbound/App.swift
@@ -19,6 +19,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
_ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
sb_set_coordinator(coordinate)
+ sb_set_sync_wake { DispatchQueue.main.async { NotificationCenter.default.post(name: Sync.changed, object: nil) } }
#if DEBUG
Presenter.watch()
#endif
@@ -77,11 +78,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro
split.setViewController(pages, for: .supplementary)
split.setViewController(UIViewController(), for: .secondary)
notebooks.onOpen = { [weak self] tab, notebook in self?.open(tab, of: notebook) }
- pages.onOpen = { [weak self] section, id, find in self?.show(section: section, page: id, find: find) }
- pages.onOpenSection = { [weak self] notebook, path, id, find in
- guard let tab = notebook.tabs.first(where: { $0.path == path }) else { return }
- self?.open(tab, of: notebook, page: id, find: find)
- }
+ pages.onOpen = { [weak self] section, id in self?.show(section: section, page: id) }
window.rootViewController = split
window.makeKeyAndVisible()
self.window = window
@@ -116,11 +113,13 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro
}
func open(
- _ tab: Tab, of notebook: Notebook, page: String? = nil, find: String? = nil,
+ _ tab: Tab, of notebook: Notebook, page: String? = nil, reveal: Reveal? = nil,
then: ((Section) -> Void)? = nil
) {
guard Author.name != nil else {
- return Author.ask(from: split) { [weak self] in self?.open(tab, of: notebook, page: page, find: find, then: then) }
+ return Author.ask(from: split) { [weak self] in
+ self?.open(tab, of: notebook, page: page, reveal: reveal, then: then)
+ }
}
pages.loading(tab)
split.show(.supplementary)
@@ -134,15 +133,37 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewContro
notebooks.select(tab, of: notebook)
Place.saved = Place(notebook: notebook.id, section: tab.path, page: page)
if let page, section.row(of: page) != nil {
- show(section: section, page: page, find: find)
+ show(section: section, page: page, reveal: reveal)
}
then?(section)
}
}
- func show(section: Section, page: String, find: String? = nil, titleFocus: Bool = false) {
+ /// Opens `page` of the section at catalog `path` in `notebook`, as a search result or
+ /// the Tags Summary leads there.
+ func open(_ path: String, of notebook: Notebook, page: String, reveal: Reveal) {
+ if let section = pages.section, section.notebook === notebook, section.tab.path == path {
+ return show(section: section, page: page, reveal: reveal)
+ }
+ guard let tab = notebook.tabs.first(where: { $0.path == path }) else { return }
+ open(tab, of: notebook, page: page, reveal: reveal)
+ }
+
+ /// OneNote's Tags Summary for `notebook`, as a sheet over `controller`.
+ func showTags(of notebook: Notebook, from controller: UIViewController) {
+ let tags = TagsViewController(notebook: notebook)
+ tags.onOpen = { [weak self] notebook, path, page, paragraph in
+ self?.open(path, of: notebook, page: page, reveal: .paragraph(paragraph))
+ }
+ let navigation = UINavigationController(rootViewController: tags)
+ navigation.sheetPresentationController?.detents = [.medium(), .large()]
+ navigation.sheetPresentationController?.prefersGrabberVisible = true
+ controller.present(navigation, animated: true)
+ }
+
+ func show(section: Section, page: String, reveal: Reveal? = nil, titleFocus: Bool = false) {
showingPage = true
- let controller = PageViewController(section: section, page: page, find: find, titleFocus: titleFocus)
+ let controller = PageViewController(section: section, page: page, reveal: reveal, titleFocus: titleFocus)
controller.onOpen = { [weak self] id in self?.show(section: section, page: id) }
split.setViewController(UINavigationController(rootViewController: controller), for: .secondary)
split.show(.secondary)
@@ -204,7 +225,8 @@ final class RootViewController: UISplitViewController {
[
UIKeyCommand(title: "New Page", action: #selector(newPage), input: "n", modifierFlags: .command),
UIKeyCommand(
- title: "New Subpage", action: #selector(newSubpage), input: "n", modifierFlags: [.command, .shift]),
+ title: "New Subpage", action: #selector(newSubpage), input: "n",
+ modifierFlags: [.command, .shift, .alternate]),
UIKeyCommand(title: "Search", action: #selector(search), input: "f", modifierFlags: [.command, .alternate]),
]
}
diff --git a/apps/ios/Snowbound/CanvasView.swift b/apps/ios/Snowbound/CanvasView.swift
index 3ed7f64baebb62122e3d5f047ac13caf88cd4918..a3e3a588c8bad5ad0941d56f8149b0f1c7947d43 100644
--- a/apps/ios/Snowbound/CanvasView.swift
+++ b/apps/ios/Snowbound/CanvasView.swift
@@ -106,6 +106,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
/// gestures move; the canvas paints neither.
private lazy var display = UITextSelectionDisplayInteraction(textInput: self, delegate: self)
private let history = CanvasUndoManager()
+ private let spaceHint = Hint()
weak var inputDelegate: UITextInputDelegate?
lazy var tokenizer: UITextInputTokenizer = LineTokenizer(canvas: self)
@@ -134,6 +135,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
minimumZoomScale = 0.25
maximumZoomScale = 4
keyboardDismissMode = .interactive
+ // A phone on its side keeps the page clear of the sensor housing, as it keeps the top.
+ contentInsetAdjustmentBehavior = .always
addSubview(content)
insertSubview(metal, at: 0)
// 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
default:
display.isActivated = isFirstResponder
edit(external: true) { sb_view_release(handle) }
+ spaceHint.hide()
}
}
@@ -512,7 +516,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
/// Tells the bar buttons and the format bar what the selection now has.
private func changedSelection() {
- if let handle { formatBar.show(sb_view_format(handle)) }
+ formatBar.show(format)
onChange?()
}
@@ -576,14 +580,20 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
}
}
+ /// A page selection copies every outline, as OneNote 2010 copies it.
override func copy(_ sender: Any?) {
- guard let range = selectedTextRange, let text = text(in: range) else { return }
+ guard let handle, let text = take(sb_view_copy(handle, false)) else { return }
UIPasteboard.general.string = text
}
override func cut(_ sender: Any?) {
- copy(sender)
- delete(sender)
+ guard let handle else { return }
+ var text: String?
+ edit(external: true) {
+ text = take(sb_view_copy(handle, true))
+ return text != nil
+ }
+ if let text { UIPasteboard.general.string = text }
}
override func delete(_ sender: Any?) {
@@ -614,6 +624,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
override func toggleItalics(_ sender: Any?) { apply(1) }
override func toggleUnderline(_ sender: Any?) { apply(2) }
+ /// Tags and the highlighter join the system's text actions.
+ func editMenu(for textRange: UITextRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
+ guard let handle else { return nil }
+ let bits = sb_view_format(handle)
+ let tag = UIMenu(
+ title: "Tag", image: UIImage(systemName: "tag"),
+ children: Tags.menu(bits: bits) { [weak self] in self?.apply($0) })
+ var extra: [UIMenuElement] = [tag]
+ if !textRange.isEmpty {
+ extra.insert(
+ UIAction(title: "Highlight", image: UIImage(systemName: "highlighter")) { [weak self] _ in
+ self?.apply(10)
+ }, at: 0)
+ }
+ return UIMenu(children: suggestedActions + [UIMenu(options: .displayInline, children: extra)])
+ }
+
// MARK: Page
/// A conflict page, which shows what is stored and takes no edits.
@@ -624,6 +651,55 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
var pageText: String { handle.flatMap { take(sb_view_page_text($0)) } ?? "" }
+ /// The selection's formatting, from `sb_view_format`.
+ var format: UInt64 { handle.map(sb_view_format) ?? 0 }
+
+ var pagePaper: Paper? { handle.flatMap { decode(Paper.self, sb_view_paper($0)) } }
+
+ /// Gives the page colour `color` and rule lines `ruled` of `paper.rules`, or none.
+ func setPaper(color: [UInt8]?, ruled: Int?) {
+ guard let handle else { return }
+ edit(external: true) {
+ sb_view_set_paper(
+ handle, color.map { Int16($0[0]) } ?? -1, color?[1] ?? 0, color?[2] ?? 0, Int8(ruled ?? -1))
+ }
+ }
+
+ /// Puts template `name`'s art behind the page, or none.
+ func setArt(_ name: String?) {
+ guard let handle else { return }
+ edit(external: true) { sb_view_set_art(handle, name) }
+ }
+
+ /// Insert Space: the next drag moves what lies below or right of where it starts.
+ func insertSpace() {
+ guard let handle else { return }
+ sb_view_insert_space(handle)
+ spaceHint.show("Drag down or right to add space", in: self)
+ }
+
+ /// Types the date, time or both as the system writes them short, and a space, as
+ /// OneNote's Insert Date and Time do.
+ func insertDate(_ date: Bool, time: Bool) {
+ let now = Date()
+ let text = DateFormatter.localizedString(
+ from: now, dateStyle: date ? .short : .none, timeStyle: time ? .short : .none)
+ if !isFirstResponder { _ = becomeFirstResponder() }
+ insertText(text + " ")
+ }
+
+ /// Selects paragraph `id` and brings it into view.
+ func selectParagraph(_ id: String) -> Bool {
+ guard let handle else { return false }
+ var found = false
+ edit(external: true) {
+ found = sb_view_select_paragraph(handle, id)
+ return found
+ }
+ if found { reveal(selectedTextRange.map { firstRect(for: $0) } ?? .zero) }
+ return found
+ }
+
/// Applies `sb_view_apply` formatting to the selection.
func apply(_ command: UInt8) {
guard let handle else { return }
@@ -682,26 +758,38 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI
override var keyCommands: [UIKeyCommand]? {
guard isFirstResponder else { return nil }
// Ctrl+1 to Ctrl+9 in OneNote, where the Mac's Command stands for Control.
- let tags = FormatBar.tags.enumerated().map { index, name in
- let command = UIKeyCommand(
- title: name, action: #selector(tagged), input: "\(index + 1)", modifierFlags: .command,
- propertyList: index)
- command.wantsPriorityOverSystemBehavior = true
- return command
+ let tags = Tags.all.prefix(Tags.keyed).enumerated().map { index, tag in
+ UIKeyCommand(
+ title: tag.name, action: #selector(formatted), input: "\(index + 1)", modifierFlags: .command,
+ propertyList: 16 + index)
}
- let outdent = UIKeyCommand(
- title: "Decrease Indent", action: #selector(outdented), input: "\t", modifierFlags: .shift)
- outdent.wantsPriorityOverSystemBehavior = true
+ // The desktop's chords, as OneNote for Mac's.
+ let formats: [(String, String, UIKeyModifierFlags, Int)] = [
+ ("Strikethrough", "x", [.command, .shift], 3),
+ ("Bullets", ".", .command, 4),
+ ("Numbering", "/", .command, 5),
+ ("Increase Indent", "]", .command, 6),
+ ("Decrease Indent", "[", .command, 7),
+ ("Remove Tag", "0", [.command, .control], 8),
+ ("Clear Formatting", "n", [.command, .shift], 9),
+ ("Highlight", "h", [.command, .control], 10),
+ ("Decrease Indent", "\t", .shift, 7),
+ ]
+ let commands = tags + formats.map { title, input, modifiers, command in
+ UIKeyCommand(
+ title: title, action: #selector(formatted), input: input, modifierFlags: modifiers,
+ propertyList: command)
+ }
+ for command in commands { command.wantsPriorityOverSystemBehavior = true }
let done = UIKeyCommand(title: "Done", action: #selector(finished), input: UIKeyCommand.inputEscape)
- return tags + [outdent, done]
+ return commands + [done]
}
- @objc private func tagged(_ command: UIKeyCommand) {
- guard let index = command.propertyList as? Int else { return }
- apply(16 + UInt8(index))
+ @objc private func formatted(_ command: UIKeyCommand) {
+ guard let command = command.propertyList as? Int else { return }
+ apply(UInt8(command))
}
- @objc private func outdented() { apply(7) }
@objc private func finished() { _ = resignFirstResponder() }
// MARK: Coordinates
@@ -948,3 +1036,47 @@ private final class LineTokenizer: UITextInputStringTokenizer {
granularity == .line || super.isPosition(position, withinTextUnit: granularity, inDirection: direction)
}
}
+
+/// A short instruction floating at the bottom of the page until the gesture it asks for.
+private final class Hint: UIVisualEffectView {
+ private let label = UILabel()
+
+ init() {
+ super.init(effect: UIBlurEffect(style: .systemThickMaterial))
+ label.font = .preferredFont(forTextStyle: .subheadline)
+ label.textAlignment = .center
+ label.translatesAutoresizingMaskIntoConstraints = false
+ contentView.addSubview(label)
+ NSLayoutConstraint.activate([
+ label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
+ label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
+ label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
+ label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
+ ])
+ layer.cornerRadius = 18
+ clipsToBounds = true
+ isUserInteractionEnabled = false
+ }
+
+ required init?(coder: NSCoder) { fatalError() }
+
+ /// Shows `text` over the bottom of `view`'s visible area, and says it.
+ func show(_ text: String, in view: UIScrollView) {
+ label.text = text
+ guard let host = view.superview else { return }
+ translatesAutoresizingMaskIntoConstraints = false
+ host.addSubview(self)
+ NSLayoutConstraint.activate([
+ centerXAnchor.constraint(equalTo: host.centerXAnchor),
+ bottomAnchor.constraint(equalTo: host.keyboardLayoutGuide.topAnchor, constant: -16),
+ ])
+ alpha = 0
+ UIView.animate(withDuration: 0.2) { self.alpha = 1 }
+ UIAccessibility.post(notification: .announcement, argument: text)
+ }
+
+ func hide() {
+ guard superview != nil else { return }
+ UIView.animate(withDuration: 0.2, animations: { self.alpha = 0 }) { _ in self.removeFromSuperview() }
+ }
+}
diff --git a/apps/ios/Snowbound/FormatBar.swift b/apps/ios/Snowbound/FormatBar.swift
index 56ff25f1127105b2ed585a93ef0855b1ed301167..926c1da50065f09b965881b60a61b067396414e0 100644
--- a/apps/ios/Snowbound/FormatBar.swift
+++ b/apps/ios/Snowbound/FormatBar.swift
@@ -1,7 +1,7 @@
import UIKit
-/// Formatting above the keyboard, as Notes offers it: tags and lists first, then text
-/// styles, indentation, tags and pictures, the keyboard's dismissal at the end.
+/// Formatting above the keyboard, as Notes offers it: To Do first, then text styles, lists,
+/// indentation, tags and pictures, the keyboard's dismissal at the end.
final class FormatBar: UIInputView {
/// `sb_view_apply` commands and `sb_view_format` bits.
private static let buttons: [(symbol: String, label: String, command: UInt8)] = [
@@ -10,18 +10,14 @@ final class FormatBar: UIInputView {
("italic", "Italic", 1),
("underline", "Underline", 2),
("strikethrough", "Strikethrough", 3),
+ ("highlighter", "Highlight", 10),
("list.bullet", "Bullets", 4),
("list.number", "Numbering", 5),
("decrease.indent", "Decrease Indent", 7),
("increase.indent", "Increase Indent", 6),
]
- /// OneNote 2010's default tags in their order, Ctrl+1 to Ctrl+9.
- static let tags = [
- "To Do", "Important", "Question", "Remember for Later", "Definition", "Highlight", "Contact", "Address",
- "Phone Number",
- ]
-
private var toggles: [(UIButton, UInt8)] = []
+ private var bits: UInt64 = 0
var onApply: ((UInt8) -> Void)?
/// Asks for a picture from the camera (true) or the photo library.
var onPicture: ((Bool) -> Void)?
@@ -44,9 +40,11 @@ final class FormatBar: UIInputView {
row.addArrangedSubview(button)
}
let tag = Self.button("tag", "Tags")
- tag.menu = UIMenu(children: Self.tags.enumerated().map { index, name in
- UIAction(title: name) { [weak self] _ in self?.onApply?(16 + UInt8(index)) }
- })
+ tag.menu = UIMenu(children: [
+ UIDeferredMenuElement.uncached { [weak self] done in
+ done(Tags.menu(bits: self?.bits ?? 0) { self?.onApply?($0) })
+ }
+ ])
tag.showsMenuAsPrimaryAction = true
row.addArrangedSubview(tag)
let picture = Self.button("photo", "Insert Picture")
@@ -105,9 +103,10 @@ final class FormatBar: UIInputView {
}
/// Marks what the selection has, from `sb_view_format`.
- func show(_ bits: UInt32) {
+ func show(_ bits: UInt64) {
+ self.bits = bits
for (button, command) in toggles {
- button.isSelected = bits & (1 << UInt32(command)) != 0
+ button.isSelected = bits & (1 << UInt64(command)) != 0
}
}
}
diff --git a/apps/ios/Snowbound/Lists.swift b/apps/ios/Snowbound/Lists.swift
index c7590ae1b346196bb0f3fc1a2add3bb5f3a9a518..11597dcb29136324d96d479f375634578adf3c5d 100644
--- a/apps/ios/Snowbound/Lists.swift
+++ b/apps/ios/Snowbound/Lists.swift
@@ -39,11 +39,14 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele
])
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "Add Notebook", image: UIImage(systemName: "plus"), menu: add)
+ navigationItem.searchController = SearchViewController.controller()
+ toolbarItems = [.flexibleSpace(), UIBarButtonItem(customView: SyncIndicator()), .flexibleSpace()]
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
+ navigationController?.setToolbarHidden(false, animated: false)
}
func reload() {
@@ -170,6 +173,10 @@ final class NotebooksViewController: UITableViewController, UIDocumentPickerDele
UIAction(title: "New Section…", image: UIImage(systemName: "plus.rectangle.portrait")) {
[weak self] _ in self?.newSection(in: notebook)
},
+ UIAction(title: "Tags Summary", image: UIImage(systemName: "tag")) { [weak self] _ in
+ guard let self else { return }
+ (view.window?.windowScene?.delegate as? SceneDelegate)?.showTags(of: notebook, from: self)
+ },
UIAction(title: "Close Notebook", image: UIImage(systemName: "xmark.circle"), attributes: .destructive) {
[weak self] _ in
Notebooks.remove(notebook)
@@ -250,7 +257,7 @@ extension UIFont {
}
/// A section's pages, subpages indented, each page's conflict pages beneath it.
-final class PagesViewController: UITableViewController, UISearchResultsUpdating {
+final class PagesViewController: UITableViewController {
private struct Item {
let row: Row
let version: Row.Version?
@@ -260,13 +267,9 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating
private(set) var section: Section?
private var items: [Item] = []
private var selected: String?
- private let results = SearchViewController()
- private lazy var search = UISearchController(searchResultsController: results)
- private var searching: DispatchWorkItem?
+ private let search = SearchViewController.controller()
/// Opens a page of the section.
- var onOpen: ((Section, String, String?) -> Void)?
- /// Opens a page of another section of the notebook, found by search.
- var onOpenSection: ((Notebook, String, String, String?) -> Void)?
+ var onOpen: ((Section, String) -> Void)?
init() {
super.init(style: .plain)
@@ -277,22 +280,14 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "page")
- search.searchResultsUpdater = self
- search.searchBar.placeholder = "Search Notebook"
navigationItem.searchController = search
navigationItem.hidesSearchBarWhenScrolling = false
- results.onOpen = { [weak self] found, query in
- guard let self, let section, let notebook = Optional(section.notebook) else { return }
- if found.section == section.tab.path {
- onOpen?(section, found.page, query)
- } else {
- onOpenSection?(notebook, found.section, found.page, query)
- }
- }
let compose = UIBarButtonItem(
title: "New Page", image: UIImage(systemName: "square.and.pencil"),
primaryAction: UIAction { [weak self] _ in self?.newPage(under: nil) })
- toolbarItems = [.flexibleSpace(), compose]
+ toolbarItems = [
+ .flexibleSpace(), UIBarButtonItem(customView: SyncIndicator()), .flexibleSpace(), compose,
+ ]
NotificationCenter.default.addObserver(
self, selector: #selector(changed), name: Section.changed, object: nil)
}
@@ -398,7 +393,7 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let section else { return }
selected = items[indexPath.row].id
- onOpen?(section, items[indexPath.row].id, nil)
+ onOpen?(section, items[indexPath.row].id)
}
override func tableView(
@@ -432,80 +427,100 @@ final class PagesViewController: UITableViewController, UISearchResultsUpdating
return UIMenu(children: actions)
})
}
+}
+
+/// A page `sb_library_search` found.
+struct Found: Decodable {
+ let section: String
+ let page: String
+ let title: String
+ let snippet: String
+}
+
+/// Every open notebook's pages holding a query, grouped by notebook, as Notes searches
+/// every folder.
+final class SearchViewController: UITableViewController, UISearchResultsUpdating {
+ private var found: [(notebook: Notebook, pages: [Found])] = []
+ private var query = ""
+ private var searching: DispatchWorkItem?
+
+ /// A search bar whose results this shows.
+ static func controller() -> UISearchController {
+ let results = SearchViewController(style: .insetGrouped)
+ let search = UISearchController(searchResultsController: results)
+ search.searchResultsUpdater = results
+ search.searchBar.placeholder = "Search Notebooks"
+ return search
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "found")
+ }
func updateSearchResults(for controller: UISearchController) {
searching?.cancel()
let query = controller.searchBar.text?.trimmingCharacters(in: .whitespaces) ?? ""
- guard let section, let library = section.notebook.handle, !query.isEmpty else {
- results.show([], query: query)
- return
- }
- // Each keystroke waits for the next before the notebook is read.
+ guard !query.isEmpty else { return show([], query: query) }
+ // Each keystroke waits for the next before the notebooks are read.
let work = DispatchWorkItem { [weak self] in
- let (library, open, path) = (Int(bitPattern: library), Int(bitPattern: section.handle), section.tab.path)
- self?.results.searching(query)
+ if self?.found.isEmpty == true {
+ var loading = UIContentUnavailableConfiguration.loading()
+ loading.text = "Searching…"
+ self?.contentUnavailableConfiguration = loading
+ }
+ let notebooks = Notebooks.all.compactMap { notebook -> (Notebook, Int, Int, String?)? in
+ guard let library = notebook.handle else { return nil }
+ // Open sections are searched as their edits leave them.
+ let open = Section.all.first { $0.notebook === notebook }
+ return (notebook, Int(bitPattern: library), open.map { Int(bitPattern: $0.handle) } ?? 0, open?.tab.path)
+ }
background({
- decode(
- [Found].self,
- sb_library_search(
- OpaquePointer(bitPattern: library), OpaquePointer(bitPattern: open), path, query)) ?? []
+ notebooks.map { notebook, library, open, path in
+ let found = decode(
+ [Found].self,
+ sb_library_search(
+ OpaquePointer(bitPattern: library), OpaquePointer(bitPattern: open), path, query))
+ return (notebook: notebook, pages: found ?? [])
+ }
}) { [weak self] found in
guard controller.searchBar.text?.trimmingCharacters(in: .whitespaces) == query else { return }
- self?.results.show(
- found, query: query,
- sections: Dictionary(
- section.notebook.tabs.map { ($0.path, $0.name) }, uniquingKeysWith: { first, _ in first }))
+ self?.show(found.filter { !$0.pages.isEmpty }, query: query)
}
}
searching = work
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work)
}
-}
-/// A page `sb_library_search` found.
-struct Found: Decodable {
- let section: String
- let page: String
- let title: String
- let snippet: String
-}
-
-final class SearchViewController: UITableViewController {
- private var found: [Found] = []
- private var sections: [String: String] = [:]
- private var query = ""
- var onOpen: ((Found, String) -> Void)?
-
- override func viewDidLoad() {
- super.viewDidLoad()
- tableView.register(UITableViewCell.self, forCellReuseIdentifier: "found")
- }
-
- func searching(_ query: String) {
- guard found.isEmpty else { return }
- var loading = UIContentUnavailableConfiguration.loading()
- loading.text = "Searching…"
- contentUnavailableConfiguration = loading
- }
-
- func show(_ found: [Found], query: String, sections: [String: String] = [:]) {
+ private func show(_ found: [(notebook: Notebook, pages: [Found])], query: String) {
self.found = found
self.query = query
- self.sections = sections
tableView.reloadData()
- contentUnavailableConfiguration = found.isEmpty && !query.isEmpty ? UIContentUnavailableConfiguration.search() : nil
+ contentUnavailableConfiguration =
+ found.isEmpty && !query.isEmpty ? UIContentUnavailableConfiguration.search() : nil
}
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { found.count }
+ override func numberOfSections(in tableView: UITableView) -> Int { found.count }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ found[section].notebook.name
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ found[section].pages.count
+ }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let found = found[indexPath.row]
+ let (notebook, pages) = found[indexPath.section]
+ let page = pages[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "found", for: indexPath)
var content = UIListContentConfiguration.subtitleCell()
- content.text = found.title.isEmpty ? "Untitled Page" : found.title
- let section = sections[found.section] ?? ""
+ content.text = page.title.isEmpty ? "Untitled Page" : page.title
+ let tab = notebook.tabs.first { $0.path == page.section }
+ content.image = UIImage(systemName: "rectangle.portrait.fill")
+ content.imageProperties.tintColor = tab?.uiColor ?? .systemGray
content.secondaryAttributedText = highlighted(
- [section, found.snippet].filter { !$0.isEmpty }.joined(separator: " · "))
+ [tab?.name ?? "", page.snippet].filter { !$0.isEmpty }.joined(separator: " · "))
content.secondaryTextProperties.numberOfLines = 2
cell.contentConfiguration = content
return cell
@@ -526,6 +541,9 @@ final class SearchViewController: UITableViewController {
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- onOpen?(found[indexPath.row], query)
+ let (notebook, pages) = found[indexPath.section]
+ let page = pages[indexPath.row]
+ (view.window?.windowScene?.delegate as? SceneDelegate)?
+ .open(page.section, of: notebook, page: page.page, reveal: .text(query))
}
}
diff --git a/apps/ios/Snowbound/Notebooks.swift b/apps/ios/Snowbound/Notebooks.swift
index 6cec50e0b6977f18e3ee0b55a9d1135b8f445cb5..89e75a8eabd3b6ded4ebe0a3401653c9859833ff 100644
--- a/apps/ios/Snowbound/Notebooks.swift
+++ b/apps/ios/Snowbound/Notebooks.swift
@@ -209,10 +209,27 @@ final class Notebook {
if let handle { sb_library_free(handle) }
handle = library
self.problem = library == nil ? (problem ?? "The notebook can’t be read.") : nil
+ follow()
reload(done: done)
}
}
+ /// Works offline or online, as Work Offline stands.
+ func follow() {
+ if let handle { sb_library_set_offline(handle, Sync.offline) }
+ }
+
+ /// Where the notebook is kept, as the sync sheet names it.
+ var location: String {
+ switch source {
+ case .files: "In Files"
+ case .documents: "On this \(UIDevice.current.model), in Snowbound’s folder"
+ case .server(let server):
+ "smb://\(server.host)/\([server.share, server.root].filter { !$0.isEmpty }.joined(separator: "/"))"
+ case .path(let path): path
+ }
+ }
+
/// Lists the sections again, off the main thread.
func reload(done: @escaping () -> Void) {
guard let handle else {
diff --git a/apps/ios/Snowbound/PageViewController.swift b/apps/ios/Snowbound/PageViewController.swift
index 8b35e356073c6998f3cc0547bdeee476b12604b6..58ea550c72d33c27e9229f3281dde9dc58dfafb5 100644
--- a/apps/ios/Snowbound/PageViewController.swift
+++ b/apps/ios/Snowbound/PageViewController.swift
@@ -1,6 +1,14 @@
import PhotosUI
import UIKit
+/// What a page shows selected as it opens.
+enum Reveal {
+ /// The first match of a search.
+ case text(String)
+ /// A paragraph the Tags Summary lists, by id.
+ case paragraph(String)
+}
+
final class PageViewController: UIViewController, PHPickerViewControllerDelegate,
UIImagePickerControllerDelegate, UINavigationControllerDelegate
{
@@ -9,7 +17,7 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
private let section: Section
private let page: String
- private let find: String?
+ private let reveal: Reveal?
private let titleFocus: Bool
private lazy var canvas = CanvasView(section: section, page: page)
private let bar = ConflictBar()
@@ -21,14 +29,18 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
systemItem: .undo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: false) })
private lazy var redo = UIBarButtonItem(
systemItem: .redo, primaryAction: UIAction { [weak self] _ in self?.canvas.undo(redo: true) })
+ private lazy var more = UIBarButtonItem(
+ title: "Page", image: UIImage(systemName: "ellipsis.circle"),
+ menu: UIMenu(children: [UIDeferredMenuElement.uncached { [weak self] done in done(self?.pageMenu() ?? []) }]))
- init(section: Section, page: String, find: String? = nil, titleFocus: Bool = false) {
+ init(section: Section, page: String, reveal: Reveal? = nil, titleFocus: Bool = false) {
self.section = section
self.page = page
- self.find = find
+ self.reveal = reveal
self.titleFocus = titleFocus
super.init(nibName: nil, bundle: nil)
navigationItem.largeTitleDisplayMode = .never
+ navigationItem.rightBarButtonItems = [more]
showTitle(section.row(of: page)?.row.title ?? "")
}
@@ -60,13 +72,21 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
self, selector: #selector(sectionChanged), name: Section.changed, object: section)
NotificationCenter.default.addObserver(self, selector: #selector(leaving), name: Self.leaving, object: nil)
conflictChanged()
+ registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (controller: PageViewController, _) in
+ controller.fitTitle()
+ }
}
- /// The page is on screen: a found word is selected, a new page's title takes the caret.
+ /// The page is on screen: a found word or tagged paragraph is selected, a new page's
+ /// title takes the caret.
private func opened() {
- if let find, canvas.find(find) {
- _ = canvas.becomeFirstResponder()
- } else if titleFocus, canvas.focusTitle() {
+ let revealed =
+ switch reveal {
+ case .text(let query): canvas.find(query)
+ case .paragraph(let id): canvas.selectParagraph(id)
+ case nil: false
+ }
+ if revealed || titleFocus && canvas.focusTitle() {
_ = canvas.becomeFirstResponder()
}
}
@@ -75,23 +95,92 @@ final class PageViewController: UIViewController, PHPickerViewControllerDelegate
self.title = title.isEmpty ? "Untitled Page" : title
}
+ /// A phone's bar has no room for the title beside the editing buttons, and the page
+ /// shows it anyway, as Notes leaves it out.
+ private func fitTitle() {
+ let crowded = canvas.isFirstResponder && traitCollection.horizontalSizeClass == .compact
+ navigationItem.titleView = crowded ? UIView() : nil
+ }
+
/// Notes' checkmark ends editing and puts the keyboard away; undo and redo sit beside it.
private func editingChanged() {
if !canvas.readOnly, let title = canvas.pageTitle {
showTitle(title)
section.retitle(page, title)
}
+ fitTitle()
guard canvas.isFirstResponder else {
- navigationItem.rightBarButtonItems = nil
+ if navigationItem.rightBarButtonItems?.first !== more { navigationItem.rightBarButtonItems = [more] }
return
}
undo.isEnabled = canvas.canUndo(redo: false)
redo.isEnabled = canvas.canUndo(redo: true)
if navigationItem.rightBarButtonItems?.first !== done {
- navigationItem.rightBarButtonItems = [done, redo, undo]
+ navigationItem.rightBarButtonItems = [done, more, redo, undo]
}
}
+ // MARK: Page menu
+
+ /// The desktop's page and insert commands that suit a touch screen.
+ private func pageMenu() -> [UIMenuElement] {
+ let editable: UIMenuElement.Attributes = canvas.readOnly ? .disabled : []
+ let scene = view.window?.windowScene?.delegate as? SceneDelegate
+ let insert = UIMenu(
+ title: "Insert", image: UIImage(systemName: "plus.circle"),
+ children: [
+ UIAction(title: "Photo Library", image: UIImage(systemName: "photo.on.rectangle"), attributes: editable) {
+ [weak self] _ in self?.pickPicture(camera: false)
+ },
+ UIAction(
+ title: "Take Photo", image: UIImage(systemName: "camera"),
+ attributes: UIImagePickerController.isSourceTypeAvailable(.camera) ? editable : .disabled
+ ) { [weak self] _ in self?.pickPicture(camera: true) },
+ UIMenu(options: .displayInline, children: [
+ UIAction(title: "Date", image: UIImage(systemName: "calendar"), attributes: editable) {
+ [weak self] _ in self?.canvas.insertDate(true, time: false)
+ },
+ UIAction(title: "Time", image: UIImage(systemName: "clock"), attributes: editable) {
+ [weak self] _ in self?.canvas.insertDate(false, time: true)
+ },
+ UIAction(title: "Date & Time", image: UIImage(systemName: "calendar.badge.clock"), attributes: editable) {
+ [weak self] _ in self?.canvas.insertDate(true, time: true)
+ },
+ ]),
+ UIAction(
+ title: "Space", image: UIImage(systemName: "arrow.up.and.down.text.horizontal"), attributes: editable
+ ) { [weak self] _ in self?.canvas.insertSpace() },
+ ])
+ return [
+ insert,
+ UIAction(title: "Page Background…", image: UIImage(systemName: "paintpalette"), attributes: editable) {
+ [weak self] _ in self?.showPaper()
+ },
+ UIAction(title: "Tags Summary", image: UIImage(systemName: "tag")) { [weak self] _ in
+ guard let self else { return }
+ scene?.showTags(of: section.notebook, from: self)
+ },
+ UIMenu(options: .displayInline, children: [
+ UIAction(title: "New Subpage", image: UIImage(systemName: "text.badge.plus")) { _ in
+ scene?.newPage(subpage: true)
+ },
+ UIAction(title: "Delete Page", image: UIImage(systemName: "trash"), attributes: .destructive) {
+ [weak self] _ in
+ guard let self else { return }
+ section.delete(page) { _ in }
+ },
+ ]),
+ ]
+ }
+
+ private func showPaper() {
+ let navigation = UINavigationController(rootViewController: PaperViewController(canvas: canvas))
+ navigation.sheetPresentationController?.detents = [.medium(), .large()]
+ navigation.sheetPresentationController?.prefersGrabberVisible = true
+ navigation.sheetPresentationController?.largestUndimmedDetentIdentifier = .medium
+ present(navigation, animated: true)
+ }
+
@objc private func leaving() { canvas.commitComposition() }
@objc private func sectionChanged(_ notification: Notification) {
diff --git a/apps/ios/Snowbound/PaperViewController.swift b/apps/ios/Snowbound/PaperViewController.swift
new file mode 100644
index 0000000000000000000000000000000000000000..847e24fd942f70b93de17e54acbe717f5d7ab3b0
--- /dev/null
+++ b/apps/ios/Snowbound/PaperViewController.swift
@@ -0,0 +1,183 @@
+import UIKit
+
+/// The page's paper from `sb_view_paper`, with the choices OneNote offers.
+struct Paper: Decodable {
+ struct Color: Decodable {
+ let name: String
+ let rgb: [UInt8]
+
+ init(from decoder: Decoder) throws {
+ var pair = try decoder.unkeyedContainer()
+ name = try pair.decode(String.self)
+ rgb = try pair.decode([UInt8].self)
+ }
+ }
+
+ let color: [UInt8]?
+ let ruled: Int?
+ let colors: [Color]
+ let rules: [String]
+ let templates: [String]
+}
+
+extension UIColor {
+ convenience init(rgb: [UInt8]) {
+ self.init(red: CGFloat(rgb[0]) / 255, green: CGFloat(rgb[1]) / 255, blue: CGFloat(rgb[2]) / 255, alpha: 1)
+ }
+
+ var rgb: [UInt8] {
+ var (red, green, blue, alpha): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
+ getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+ return [red, green, blue].map { UInt8((min(1, max(0, $0)) * 255).rounded()) }
+ }
+}
+
+/// OneNote's View, Page Color and Rule Lines, and a template's art behind the page, as a
+/// sheet over the page, each choice taking effect at once.
+final class PaperViewController: UITableViewController {
+ private enum Part: Int, CaseIterable { case color, rules, background }
+
+ private let canvas: CanvasView
+ private var paper: Paper?
+ private let swatches = UIStackView()
+ private let well = UIColorWell()
+
+ init(canvas: CanvasView) {
+ self.canvas = canvas
+ super.init(style: .insetGrouped)
+ title = "Page Background"
+ }
+
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "row")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) })
+ well.title = "Custom Color"
+ well.supportsAlpha = false
+ well.addAction(UIAction { [weak self] _ in
+ guard let self, let color = well.selectedColor else { return }
+ canvas.setPaper(color: color.rgb, ruled: paper?.ruled)
+ reload()
+ }, for: .valueChanged)
+ reload()
+ }
+
+ private func reload() {
+ paper = canvas.pagePaper
+ showSwatches()
+ tableView.reloadData()
+ }
+
+ private func showSwatches() {
+ swatches.arrangedSubviews.forEach { $0.removeFromSuperview() }
+ guard let paper else { return }
+ let none = swatch(nil, name: "No Color", chosen: paper.color == nil)
+ swatches.addArrangedSubview(none)
+ for color in paper.colors {
+ swatches.addArrangedSubview(swatch(color.rgb, name: color.name, chosen: paper.color == color.rgb))
+ }
+ let custom = paper.color.flatMap { color in paper.colors.contains { $0.rgb == color } ? nil : color }
+ well.selectedColor = custom.map(UIColor.init(rgb:))
+ swatches.addArrangedSubview(well)
+ }
+
+ private func swatch(_ rgb: [UInt8]?, name: String, chosen: Bool) -> UIButton {
+ var configuration = UIButton.Configuration.plain()
+ configuration.background.backgroundColor = rgb.map(UIColor.init(rgb:)) ?? .white
+ configuration.background.cornerRadius = 16
+ configuration.background.strokeColor = chosen ? .tintColor : .separator
+ configuration.background.strokeWidth = chosen ? 3 : 1
+ if rgb == nil {
+ configuration.image = UIImage(systemName: "line.diagonal")
+ configuration.baseForegroundColor = .systemRed
+ }
+ let button = UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in
+ guard let self else { return }
+ canvas.setPaper(color: rgb, ruled: paper?.ruled)
+ reload()
+ })
+ button.accessibilityLabel = name
+ button.accessibilityTraits.insert(chosen ? .selected : [])
+ button.widthAnchor.constraint(equalToConstant: 32).isActive = true
+ button.heightAnchor.constraint(equalToConstant: 32).isActive = true
+ return button
+ }
+
+ private let parts = Part.allCases
+
+ override func numberOfSections(in tableView: UITableView) -> Int { parts.count }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch parts[section] {
+ case .color: "Page Color"
+ case .rules: "Rule Lines"
+ case .background: "Background"
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard let paper else { return 0 }
+ return switch parts[section] {
+ case .color: 1
+ case .rules: 1 + paper.rules.count
+ case .background: 1 + paper.templates.count
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "row", for: indexPath)
+ cell.contentView.subviews.filter { $0 is UIScrollView }.forEach { $0.removeFromSuperview() }
+ cell.accessoryType = .none
+ cell.contentConfiguration = nil
+ guard let paper else { return cell }
+ switch parts[indexPath.section] {
+ case .color:
+ cell.selectionStyle = .none
+ let scroll = UIScrollView()
+ scroll.showsHorizontalScrollIndicator = false
+ swatches.spacing = 12
+ swatches.alignment = .center
+ swatches.translatesAutoresizingMaskIntoConstraints = false
+ scroll.translatesAutoresizingMaskIntoConstraints = false
+ scroll.addSubview(swatches)
+ cell.contentView.addSubview(scroll)
+ NSLayoutConstraint.activate([
+ scroll.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
+ scroll.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor),
+ scroll.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
+ scroll.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
+ scroll.heightAnchor.constraint(equalToConstant: 56),
+ swatches.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 16),
+ swatches.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -16),
+ swatches.centerYAnchor.constraint(equalTo: scroll.frameLayoutGuide.centerYAnchor),
+ ])
+ case .rules:
+ cell.selectionStyle = .default
+ var content = cell.defaultContentConfiguration()
+ content.text = indexPath.row == 0 ? "None" : paper.rules[indexPath.row - 1]
+ cell.contentConfiguration = content
+ let chosen = indexPath.row == 0 ? paper.ruled == nil : paper.ruled == indexPath.row - 1
+ cell.accessoryType = chosen ? .checkmark : .none
+ case .background:
+ cell.selectionStyle = .default
+ var content = cell.defaultContentConfiguration()
+ content.text = indexPath.row == 0 ? "None" : paper.templates[indexPath.row - 1]
+ cell.contentConfiguration = content
+ }
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+ guard let paper else { return }
+ switch parts[indexPath.section] {
+ case .color: return
+ case .rules: canvas.setPaper(color: paper.color, ruled: indexPath.row == 0 ? nil : indexPath.row - 1)
+ case .background: canvas.setArt(indexPath.row == 0 ? nil : paper.templates[indexPath.row - 1])
+ }
+ reload()
+ }
+}
diff --git a/apps/ios/Snowbound/Script.swift b/apps/ios/Snowbound/Script.swift
index d5bf52cb775dec2fc44a9701a3a95b4f328a87c7..2e52abd094eaaa191214e12a1483b878edc820bd 100644
--- a/apps/ios/Snowbound/Script.swift
+++ b/apps/ios/Snowbound/Script.swift
@@ -10,7 +10,7 @@ extension CanvasView {
/// `tap` and `doubletap` take points from the view's corner, `select` text offsets,
/// `scroll` a content offset; `format:N` applies `sb_view_apply` formatting, `find:word`
/// selects a match, `title` edits the title, `picture` inserts a drawn picture, `selectall`
- /// is the Select All command; `done`
+ /// is the Select All command; `rotate:landscape` or `rotate:portrait` turns the device; `done`
/// ends editing, `tree` saves the view hierarchy to
/// Documents/tree.txt, and `shot:a` the window to Documents/a.png, as a device has no
/// screenshot command.
@@ -54,6 +54,9 @@ extension CanvasView {
case "doubletap":
let values = argument.split(separator: ",").compactMap { Double($0) }
zoom(at: CGPoint(x: values[0] + contentOffset.x, y: values[1] + contentOffset.y))
+ case "rotate":
+ let orientation: UIInterfaceOrientationMask = argument == "portrait" ? .portrait : .landscapeRight
+ window?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: orientation))
case "done": _ = resignFirstResponder()
case "selectall": selectAll(nil)
case "format": apply(UInt8(argument) ?? 0)
diff --git a/apps/ios/Snowbound/Sync.swift b/apps/ios/Snowbound/Sync.swift
new file mode 100644
index 0000000000000000000000000000000000000000..cfa00d2b66d77a6bce55ab999f586967638f25be
--- /dev/null
+++ b/apps/ios/Snowbound/Sync.swift
@@ -0,0 +1,260 @@
+import UIKit
+
+/// A section's sync status from `sb_library_sync_status`.
+struct SectionSync: Decodable {
+ enum State: UInt8, Decodable, Comparable {
+ case upToDate, syncing, inUse, notConnected, readOnly, failed
+
+ static func < (a: State, b: State) -> Bool { a.rawValue < b.rawValue }
+ }
+
+ let path: String
+ let state: State
+ /// Seconds since 1970 when the section file was last reached.
+ let synced: Double?
+ let queued: Int
+ let error: String?
+}
+
+/// OneNote's Work Offline and Sync Now over every open notebook, and their status.
+enum Sync {
+ /// Posted on the main thread when a notebook's closed sections report.
+ static let changed = Notification.Name("SyncChanged")
+ private static let key = "offline"
+
+ static var offline: Bool {
+ get { UserDefaults.standard.bool(forKey: key) }
+ set {
+ UserDefaults.standard.set(newValue, forKey: key)
+ for notebook in Notebooks.all { notebook.follow() }
+ NotificationCenter.default.post(name: changed, object: nil)
+ }
+ }
+
+ static func now() {
+ for notebook in Notebooks.all {
+ if let handle = notebook.handle { sb_library_sync_now(handle) }
+ }
+ }
+
+ /// Every open notebook's sections with their status, read off the main thread.
+ static func status(done: @escaping ([(Notebook, [SectionSync])]) -> Void) {
+ let notebooks = Notebooks.all.compactMap { notebook in notebook.handle.map { (notebook, Int(bitPattern: $0)) } }
+ background({
+ notebooks.map { notebook, pointer in
+ (notebook, decode([SectionSync].self, sb_library_sync_status(OpaquePointer(bitPattern: pointer))) ?? [])
+ }
+ }, done: done)
+ }
+
+ /// What `state` comes to for the reader, as the desktop names it.
+ static func label(_ state: SectionSync.State) -> String {
+ if offline { return "Working offline" }
+ return switch state {
+ case .upToDate: "Up to date"
+ case .syncing: "Syncing…"
+ case .inUse: "Section in use"
+ case .notConnected: "Not connected"
+ case .readOnly, .failed: "Unable to sync"
+ }
+ }
+
+ static func symbol(_ state: SectionSync.State) -> String {
+ if offline { return "icloud.slash" }
+ return switch state {
+ case .upToDate: "checkmark.circle"
+ case .syncing, .inUse: "arrow.triangle.2.circlepath"
+ case .notConnected: "wifi.slash"
+ case .readOnly, .failed: "exclamationmark.triangle"
+ }
+ }
+
+ /// What the reader can do about `state`.
+ static func advice(_ state: SectionSync.State) -> String? {
+ let device = UIDevice.current.model
+ if offline { return "Changes stay on this \(device) until you sync." }
+ return switch state {
+ case .readOnly: "You can’t change this notebook where it’s stored. Changes stay on this \(device)."
+ case .inUse: "Someone else is saving this section. Sync continues when they finish."
+ case .notConnected: "Changes stay on this \(device) and sync when the notebook is back."
+ default: nil
+ }
+ }
+
+ static func changes(_ count: Int) -> String { count == 1 ? "1 change" : "\(count) changes" }
+
+ /// When a section was last reached: the time, and the date too before today.
+ static func when(_ seconds: Double) -> String {
+ let date = Date(timeIntervalSince1970: seconds)
+ return Calendar.current.isDateInToday(date)
+ ? DateFormatter.localizedString(from: date, dateStyle: .none, timeStyle: .short)
+ : DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .short)
+ }
+}
+
+/// The sync status in a list's toolbar, as Mail shows when it last checked; opens the sync
+/// sheet.
+final class SyncIndicator: UIButton {
+ init() {
+ var configuration = UIButton.Configuration.plain()
+ configuration.imagePadding = 4
+ configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(textStyle: .caption1)
+ configuration.baseForegroundColor = .secondaryLabel
+ configuration.titleLineBreakMode = .byTruncatingTail
+ configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer {
+ var attributes = $0
+ attributes.font = .preferredFont(forTextStyle: .caption1)
+ return attributes
+ }
+ super.init(frame: .zero)
+ self.configuration = configuration
+ addAction(UIAction { [weak self] _ in self?.present() }, for: .primaryActionTriggered)
+ for name in [Sync.changed, Section.changed] {
+ NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil)
+ }
+ refresh()
+ }
+
+ required init?(coder: NSCoder) { fatalError() }
+
+ @objc private func refresh() {
+ Sync.status { [weak self] notebooks in
+ let sections = notebooks.flatMap(\.1)
+ guard let self, let worst = sections.map(\.state).max() else {
+ self?.isHidden = true
+ return
+ }
+ isHidden = false
+ let queued = sections.map(\.queued).reduce(0, +)
+ let label = Sync.label(worst)
+ configuration?.title = queued > 0 && worst != .syncing ? "\(label), \(Sync.changes(queued))" : label
+ configuration?.image = UIImage(systemName: Sync.symbol(worst))
+ configuration?.baseForegroundColor = worst >= .notConnected && !Sync.offline ? .systemOrange : .secondaryLabel
+ accessibilityLabel = "Sync status: \(configuration?.title ?? label)"
+ sizeToFit()
+ }
+ }
+
+ private func present() {
+ guard let controller = window?.rootViewController else { return }
+ let navigation = UINavigationController(rootViewController: SyncViewController())
+ navigation.sheetPresentationController?.detents = [.medium(), .large()]
+ navigation.sheetPresentationController?.prefersGrabberVisible = true
+ (controller.presentedViewController ?? controller).present(navigation, animated: true)
+ }
+}
+
+/// OneNote's Notebook Sync Status: Work Offline, Sync Now, and each notebook's sections.
+final class SyncViewController: UITableViewController {
+ private var notebooks: [(Notebook, [SectionSync])] = []
+ private var timer: Timer?
+ private let offline = UISwitch()
+
+ init() {
+ super.init(style: .insetGrouped)
+ title = "Sync Status"
+ }
+
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "row")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) })
+ offline.addAction(UIAction { [weak self] _ in
+ Sync.offline = self?.offline.isOn ?? false
+ }, for: .valueChanged)
+ for name in [Sync.changed, Section.changed] {
+ NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: name, object: nil)
+ }
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ refresh()
+ // The last sync time follows each round, which reports only when something changes.
+ timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in self?.refresh() }
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ timer?.invalidate()
+ }
+
+ @objc private func refresh() {
+ Sync.status { [weak self] notebooks in
+ self?.notebooks = notebooks.filter { !$0.1.isEmpty }
+ self?.tableView.reloadData()
+ }
+ }
+
+ override func numberOfSections(in tableView: UITableView) -> Int { 1 + notebooks.count }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ section == 0 ? 2 : notebooks[section - 1].1.count
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ section == 0 ? nil : notebooks[section - 1].0.name
+ }
+
+ override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ guard section > 0 else { return Sync.offline ? Sync.advice(.upToDate) : nil }
+ let (notebook, sections) = notebooks[section - 1]
+ let worst = sections.map(\.state).max() ?? .upToDate
+ let synced = sections.map(\.synced)
+ let oldest = synced.contains(where: { $0 == nil }) ? nil : synced.compactMap { $0 }.min()
+ return [
+ notebook.location,
+ oldest.map { "Last sync \(Sync.when($0))" },
+ Sync.offline ? nil : Sync.advice(worst),
+ ].compactMap { $0 }.joined(separator: "\n")
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "row", for: indexPath)
+ cell.accessoryView = nil
+ cell.selectionStyle = .none
+ if indexPath.section == 0 {
+ var content = UIListContentConfiguration.cell()
+ if indexPath.row == 0 {
+ content.text = "Work Offline"
+ offline.isOn = Sync.offline
+ cell.accessoryView = offline
+ } else {
+ content.text = "Sync Now"
+ content.textProperties.color = .tintColor
+ cell.selectionStyle = .default
+ }
+ cell.contentConfiguration = content
+ return cell
+ }
+ let (notebook, sections) = notebooks[indexPath.section - 1]
+ let sync = sections[indexPath.row]
+ var content = UIListContentConfiguration.valueCell()
+ let tab = notebook.tabs.first { $0.path == sync.path }
+ content.text = tab?.name ?? URL(fileURLWithPath: sync.path).deletingPathExtension().lastPathComponent
+ content.image = UIImage(systemName: "rectangle.portrait.fill")
+ content.imageProperties.tintColor = tab?.uiColor ?? .systemGray
+ let label = Sync.label(sync.state)
+ content.secondaryText = sync.queued > 0 ? "\(label), \(Sync.changes(sync.queued))" : label
+ if let error = sync.error {
+ content = UIListContentConfiguration.subtitleCell()
+ content.text = tab?.name ?? sync.path
+ content.image = UIImage(systemName: "rectangle.portrait.fill")
+ content.imageProperties.tintColor = tab?.uiColor ?? .systemGray
+ content.secondaryText = "\(label): \(error)"
+ content.secondaryTextProperties.color = .secondaryLabel
+ }
+ cell.contentConfiguration = content
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+ guard indexPath == IndexPath(row: 1, section: 0) else { return }
+ Sync.now()
+ refresh()
+ }
+}
diff --git a/apps/ios/Snowbound/Tags.swift b/apps/ios/Snowbound/Tags.swift
new file mode 100644
index 0000000000000000000000000000000000000000..cc2676721b620785a5750ea238973328d7ece294
--- /dev/null
+++ b/apps/ios/Snowbound/Tags.swift
@@ -0,0 +1,193 @@
+import UIKit
+
+/// OneNote's tags in `sb_view_apply`'s order from 16, drawn with the page's artwork.
+enum Tags {
+ struct Tag {
+ let name: String
+ /// The symbol `sb_tag_icon` draws; 0 for a highlighting tag.
+ let shape: UInt16
+ let highlight: UIColor?
+ }
+
+ static let all: [Tag] = {
+ guard let text = take(sb_tags()),
+ let rows = try? JSONSerialization.jsonObject(with: Data(text.utf8)) as? [[Any]]
+ else { return [] }
+ return rows.compactMap { row in
+ guard row.count == 3, let name = row[0] as? String, let shape = row[1] as? NSNumber else { return nil }
+ let highlight = (row[2] as? [NSNumber]).map { UIColor(rgb: $0.map(\.uint8Value)) }
+ return Tag(name: name, shape: shape.uint16Value, highlight: highlight)
+ }
+ }()
+
+ /// The first nine, which ⌘1 to ⌘9 apply, as Ctrl+1 to Ctrl+9 do in OneNote.
+ static let keyed = 9
+
+ private static var icons: [Int: UIImage] = [:]
+
+ /// Tag symbol `shape` as OneNote draws it, a check box `checked`; a highlighter in
+ /// `highlight` for a highlighting tag.
+ static func icon(_ shape: UInt16, checked: Bool = false, highlight: UIColor? = nil) -> UIImage? {
+ guard shape != 0 else {
+ // Highlighted lines, as the desktop draws these tags.
+ let fill = highlight ?? .systemYellow
+ return UIGraphicsImageRenderer(size: CGSize(width: 18, height: 18)).image { context in
+ let card = UIBezierPath(roundedRect: CGRect(x: 1.5, y: 3.5, width: 15, height: 11), cornerRadius: 1.5)
+ fill.setFill()
+ card.fill()
+ UIColor(white: 0, alpha: 0.35).setStroke()
+ card.stroke()
+ UIColor(white: 0.25, alpha: 1).setFill()
+ context.fill(CGRect(x: 4, y: 6.5, width: 10, height: 1.5))
+ context.fill(CGRect(x: 4, y: 10, width: 7, height: 1.5))
+ }
+ }
+ let key = Int(shape) << 1 | (checked ? 1 : 0)
+ if let icon = icons[key] { return icon }
+ let points: CGFloat = 18
+ let scale = UIScreen.main.scale
+ let pixels = Int(points * scale)
+ var rgba = [UInt8](repeating: 0, count: pixels * pixels * 4)
+ guard sb_tag_icon(shape, checked, UInt32(pixels), &rgba),
+ let provider = CGDataProvider(data: Data(rgba) as CFData),
+ let image = CGImage(
+ width: pixels, height: pixels, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: pixels * 4,
+ space: CGColorSpace(name: CGColorSpace.sRGB)!,
+ bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue),
+ provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent)
+ else { return UIImage(systemName: "tag") }
+ let icon = UIImage(cgImage: image, scale: scale, orientation: .up).withRenderingMode(.alwaysOriginal)
+ icons[key] = icon
+ return icon
+ }
+
+ /// Every tag as a menu, those the selection has checked, then Remove Tag; `apply` takes
+ /// the `sb_view_apply` command.
+ static func menu(bits: UInt64, apply: @escaping (UInt8) -> Void) -> [UIMenuElement] {
+ let actions = all.enumerated().map { index, tag in
+ UIAction(
+ title: tag.name, image: icon(tag.shape, highlight: tag.highlight),
+ state: bits & (1 << UInt64(16 + index)) != 0 ? .on : .off
+ ) { _ in apply(16 + UInt8(index)) }
+ }
+ let tagged = bits >> 16 != 0
+ return [
+ UIMenu(options: .displayInline, children: Array(actions.prefix(keyed))),
+ UIMenu(title: "More Tags", image: UIImage(systemName: "tag"), children: Array(actions.dropFirst(keyed))),
+ UIAction(
+ title: "Remove Tag", image: UIImage(systemName: "tag.slash"),
+ attributes: tagged ? [] : .disabled
+ ) { _ in apply(8) },
+ ]
+ }
+}
+
+/// A tagged paragraph from `sb_library_tagged`.
+struct Tagged: Decodable {
+ let section: String
+ let page: String
+ let title: String
+ let paragraph: String
+ let name: String
+ let shape: UInt16
+ let checked: Bool
+ let text: String
+}
+
+/// OneNote's Tags Summary: a notebook's tagged paragraphs by tag, each opening its page with
+/// the paragraph selected.
+final class TagsViewController: UITableViewController {
+ private let notebook: Notebook
+ private var groups: [(name: String, items: [Tagged])] = []
+ private var unchecked = false
+ private var all: [Tagged] = []
+ /// Opens a page of the notebook: section path, page and paragraph.
+ var onOpen: ((Notebook, String, String, String) -> Void)?
+
+ init(notebook: Notebook) {
+ self.notebook = notebook
+ super.init(style: .insetGrouped)
+ title = "Tags Summary"
+ }
+
+ required init?(coder: NSCoder) { fatalError() }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tagged")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(
+ systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) })
+ navigationItem.leftBarButtonItem = UIBarButtonItem(
+ title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle"),
+ menu: UIMenu(children: [
+ UIDeferredMenuElement.uncached { [weak self] done in
+ done([
+ UIAction(title: "Unchecked Only", state: self?.unchecked == true ? .on : .off) { _ in
+ self?.unchecked.toggle()
+ self?.show()
+ }
+ ])
+ }
+ ]))
+ var loading = UIContentUnavailableConfiguration.loading()
+ loading.text = "Finding Tags…"
+ contentUnavailableConfiguration = loading
+ guard let library = notebook.handle else { return }
+ let open = Section.all.first { $0.notebook === notebook }
+ let (pointer, section, path) = (Int(bitPattern: library), open.map { Int(bitPattern: $0.handle) } ?? 0, open?.tab.path)
+ background({
+ decode([Tagged].self, sb_library_tagged(OpaquePointer(bitPattern: pointer), OpaquePointer(bitPattern: section), path))
+ ?? []
+ }) { [weak self] tagged in
+ self?.all = tagged
+ self?.show()
+ }
+ }
+
+ private func show() {
+ var order: [String] = []
+ var byName: [String: [Tagged]] = [:]
+ for tagged in all where !unchecked || !tagged.checked {
+ if byName[tagged.name] == nil { order.append(tagged.name) }
+ byName[tagged.name, default: []].append(tagged)
+ }
+ groups = order.map { ($0, byName[$0] ?? []) }
+ tableView.reloadData()
+ var empty = UIContentUnavailableConfiguration.empty()
+ empty.image = UIImage(systemName: "tag")
+ empty.text = unchecked ? "No Unchecked Tags" : "No Tags"
+ empty.secondaryText = unchecked ? nil : "Tag a paragraph from the format bar or its menu."
+ contentUnavailableConfiguration = groups.isEmpty ? empty : nil
+ }
+
+ override func numberOfSections(in tableView: UITableView) -> Int { groups.count }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ groups[section].name
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ groups[section].items.count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let tagged = groups[indexPath.section].items[indexPath.row]
+ let cell = tableView.dequeueReusableCell(withIdentifier: "tagged", for: indexPath)
+ var content = UIListContentConfiguration.subtitleCell()
+ content.image = Tags.icon(tagged.shape, checked: tagged.checked)
+ content.text = tagged.text.isEmpty ? " " : tagged.text
+ content.textProperties.numberOfLines = 3
+ let section = notebook.tabs.first { $0.path == tagged.section }?.name
+ content.secondaryText = [tagged.title.isEmpty ? "Untitled Page" : tagged.title, section]
+ .compactMap { $0 }.joined(separator: " · ")
+ content.secondaryTextProperties.color = .secondaryLabel
+ cell.contentConfiguration = content
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let tagged = groups[indexPath.section].items[indexPath.row]
+ dismiss(animated: true)
+ onOpen?(notebook, tagged.section, tagged.page, tagged.paragraph)
+ }
+}
diff --git a/arc/canvas.md b/arc/canvas.md
index 9258569391cb92a012cd45b43b950457f97af6ca..22f8e65311ad6a893d32cd6e473959a137c4b7f2 100644
--- a/arc/canvas.md
+++ b/arc/canvas.md
@@ -41,7 +41,10 @@ the size of the edit, not the size of the page.
Undo lives in the editor, not in storage. Each history entry keeps the inverse
of the change it made. Undoing applies that inverse and emits its ops as a new
-edit, so the file only ever moves forward, like OneNote's. Undoing a deletion
+edit, so the file only ever moves forward, like OneNote's. Undo steps are
+OneNote 2010's too: typing and backspaces at one caret are a single step until
+the caret moves or another kind of edit comes between, though each keystroke's
+ops still reach storage as it is typed. Undoing a deletion
brings back the original identities, so internal links to those paragraphs
survive an undo.
@@ -87,6 +90,22 @@ OneNote links it when it opens a page, without being stored as one. Ink draws st
coordinates. Page templates' background art is recreated as vector art and
recognised by the stored picture's hash. OneNote's bitmaps aren't shipped.
+## Attached files
+
+A file sits in an outline's flow as OneNote draws it: its icon over its name,
+extension hidden, in a 54-point column. Attaching splits the caret's paragraph
+around the file, and the text after the caret follows it with the caret, as
+OneNote 2010 attaches and drops files. Every new file stores the icon OneNote
+stores beside it, because OneNote draws a broken picture for a file without
+one: the host's system icon where it has one, the canvas's blank page
+otherwise. A double click or the context menu's Open asks the host to open a
+copy; Save As writes the bytes where the user chooses.
+
+Attached or dropped where a click on blank page left the caret, the file goes
+on the page itself, as OneNote 2010 places it: the same column at the caret's
+grid point, outside any outline. It selects, drags on the grid, deletes and
+moves with Insert Space as a picture does, and opens and saves as above.
+
## Content the editor doesn't understand
Nothing is lost for being unfamiliar. A paragraph the canvas can't draw
diff --git a/arc/file-format.md b/arc/file-format.md
index f8bfbeefe7d4403dbf534bebc479d450949430bf..76fbfd1bd3251b488fe3633c138eea344ba26eaa 100644
--- a/arc/file-format.md
+++ b/arc/file-format.md
@@ -54,9 +54,14 @@ the file grows):
file mean nothing until the header's transaction count says a transaction
holding them has committed.
-External payloads (large pictures, attachments, recordings) can live beside
-the section as `.onebin` files in a `_onefiles` folder. The section refers to
-them by name.
+The format lets payloads (pictures, attachments, recordings) live beside the
+section as `.onebin` files in a `_onefiles` folder, referred to by name. OneNote
+2010 never writes them: every attachment it stores, 300 MiB ones included and
+on a share too, goes into the section's own file data store, with a 32-pixel
+PNG of the file's icon beside it. Snowbound writes the same. OneNote also
+stores identical bytes once per section, so two attachments of one file (or
+one icon) share a payload; Snowbound stores each anew, which OneNote reads the
+same.
## What "append one revision" means
diff --git a/arc/sync.md b/arc/sync.md
index 0d53c438c69479bb488f8060cbce3eba52042830..426a30ff8fd5f147df3c0223c536ec0f475935f0 100644
--- a/arc/sync.md
+++ b/arc/sync.md
@@ -58,6 +58,13 @@ replayed and the open batch applied, rebuilt on open. The database runs in WAL
mode with full synchronous commits, and every setting is read back to check it
took.
+An attached file is a payload like a picture: its bytes ride in the edit that
+inserts it and publish inside that edit's revision. No other file is written,
+so nothing needs to reach the share before the revision that names it. That
+ordering would matter only for `_onefiles` payloads, which OneNote 2010 never
+writes ([file format](file-format.md)). A large file makes a publication as
+large as itself.
+
A keystroke that publishes immediately costs three commits: the edit, the seal
(which also records the publication attempt), and the receipt. Each one is
ordered against something outside the cache. The edit must be durable before
diff --git a/arc/ui.md b/arc/ui.md
index 4287b45b2d1028c9ef760e7538ed0b517c7a739d..8c9749f5a91599285930b57b5f6b42f8a3fa1215 100644
--- a/arc/ui.md
+++ b/arc/ui.md
@@ -116,7 +116,8 @@ rounded or gradient rectangles, pen strokes and raster images.
- **Glyphs** are rasterized with Swash into an atlas, at quarter-pixel phases,
so text placed at fractional positions stays crisp and doesn't shimmer as it
moves. Icons and paths rasterize once per size and phase into the same
- atlas.
+ atlas. A rounded box's soft shadow is worked out in the shader instead, so
+ popups of any size, widening or not, cost the atlas nothing.
- **The renderer never measures text.** Text reaches it through a trait that
visits positioned glyph runs. Line metrics stay with whoever laid the text
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.
or within shows through a row. A popup's contents clip to its rounded
outline, and one opening over a combo lays its rows out at their final
width while only the outline widens.
-- **Caches are bounded.** The glyph atlas and image cache have fixed budgets.
- Eviction keeps everything the current frame needs and rebuilds after
- pressure. Images are filtered in linear light with premultiplied alpha.
+- **Caches are bounded.** The image cache has a fixed budget. A full glyph
+ atlas is cleared and rebuilt with only what the current frame needs; where
+ one frame alone needs more than half of it, it doubles, up to what the GPU
+ allows. Images are filtered in linear light with premultiplied alpha.
GPU readback tests pin the rasterization down: successive quarter-pixel
translations have to move the ink's centroid in quarter-pixel steps, and
diff --git a/corpus/attachment-edit/README.md b/corpus/attachment-edit/README.md
index cb168e20325c7e9341e9e18f62d3776e6e12d523..4d550f6ea431829078d2fe0d12f083a3643d94a2 100644
--- a/corpus/attachment-edit/README.md
+++ b/corpus/attachment-edit/README.md
@@ -10,7 +10,8 @@ section's file-data store list, an embedded-file object declaring it by
identity and extension, and an attachment object with the file name, the
24 pt icon size and the flags every native attachment carries. `icon/` also
embeds the native fixture's 724-byte icon PNG as the preview picture;
-`plain/` omits it, which OneNote accepts (it renders its own icon).
+`plain/` omits it, which OneNote opens but draws as a broken picture
+(`corpus/attachment-insert/without-icon.png`), so the editor always stores one.
OneNote's integrity check refuses an attachment object without
`0x1c001d61` (twenty bytes: 16, 1, zeros), so the writer always stores it.
diff --git a/corpus/attachment-floating/README.md b/corpus/attachment-floating/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..30333ecbe18077d2898cbef468320b4c951f5675
--- /dev/null
+++ b/corpus/attachment-floating/README.md
@@ -0,0 +1,38 @@
+# Files on the page
+
+`native/files.one` is OneNote 2010's own section, made in a lab clone on
+2026-09-29. Its second page holds two copies of `float.txt` (38 bytes) outside
+any outline: one inserted through COM `UpdatePageContent` as a page-level
+`InsertedFile` at (216, 180) and then dragged to (288, 140.4), the other
+attached with Insert > Attach File after a click on blank page, at (342, 284.4).
+
+`candidate/` is `crates/canvas/tests/attachment_insert.rs`'s second section:
+the editor places the caret on blank page at (342, 284.4) and attaches
+`float 🦀.txt` (37 bytes, with the icon OneNote stored for a text file), then
+at (126, 410.4) attaches `large.bin` (2 MiB of noise, with the canvas's
+blank-page icon) and drags it to (288, 140.4). Each step is its own edit of the
+ops the editor recorded, sealed into one appended revision. `cold/` is a fresh
+OneNote 2010 read: both files are page-level `InsertedFile`s at those
+positions, and the bytes OneNote materializes to open them
+(`read/*.attachment`) hash as written. `tools/test_attachment_floating.py`
+checks the capture without a VM. Regenerate with `CANVAS_FLOATING_EXPORT` set to
+a directory while running the test, then `tools/native_runner.py DIR COLD
+--expected-pages 1 --collect-notebook --screenshots`.
+
+## What OneNote 2010 does
+
+- **Placement.** Attached or dropped on blank page, the file becomes a child of
+ the page at the point a click there puts the caret: on the 18 pt grid from
+ the margin origin, 7 px above the pointer. It draws as a file in an outline
+ does, its 32-pixel icon at 24 points centred in a 54-point column over its
+ name without the extension, and selects as a shaded column framed in dashes,
+ without handles.
+- **Storage.** The attachment object (jcid 0x60035) carries what a paragraph's
+ file does, except LayoutAlignmentInParent; its position (0x14001c14/15),
+ LayoutAlignmentSelf 9, and the column's LayoutMaxWidth/Height (54 by about 63
+ points) with the size not set by the user. A file inserted through COM has
+ neither alignment nor size.
+- **Moves.** Dragging rewrites the position alone, on the grid; the
+ modification time stays.
+- **Payloads.** Both copies' embedded-file objects, and both icons, name one
+ payload in the file data store.
diff --git a/corpus/attachment-floating/candidate/Open Notebook.onetoc2 b/corpus/attachment-floating/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..b29c1db7203d7c9d5349230df4d715f533d71a35
Binary files /dev/null and b/corpus/attachment-floating/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/attachment-floating/candidate/files.one b/corpus/attachment-floating/candidate/files.one
new file mode 100644
index 0000000000000000000000000000000000000000..905e7d4c7d1dc2720b22fedbf895ae0d43dae17f
Binary files /dev/null and b/corpus/attachment-floating/candidate/files.one differ
diff --git a/corpus/attachment-floating/cold/commands.jsonl b/corpus/attachment-floating/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..0e015ed5e18134a2f31be5ea120cf81847101b62
--- /dev/null
+++ b/corpus/attachment-floating/cold/commands.jsonl
@@ -0,0 +1,3 @@
+{"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}
+{"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}
+{"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}
diff --git a/corpus/attachment-floating/cold/machine.json b/corpus/attachment-floating/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..21fb874d077363c9691a36d3cc0b91de9f5d96aa
--- /dev/null
+++ b/corpus/attachment-floating/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-45bb9354", "hostname": "ONE-M6-45BB9354"}
diff --git a/corpus/attachment-floating/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment b/corpus/attachment-floating/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment
new file mode 100644
index 0000000000000000000000000000000000000000..8e1f70cf2640fa404f4399f624f84db6853af846
Binary files /dev/null and b/corpus/attachment-floating/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment differ
diff --git a/corpus/attachment-floating/cold/read/78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f.attachment b/corpus/attachment-floating/cold/read/78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f.attachment
new file mode 100644
index 0000000000000000000000000000000000000000..11fece2d64368f44102dcece5b19bb3d527b1ce7
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f.attachment
@@ -0,0 +1 @@
+Snowbound put this file on the page.
diff --git a/corpus/attachment-floating/cold/read/environment.json b/corpus/attachment-floating/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..4220e6ae2b32510afa9807de54fe2992ef22cf0b
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-45BB9354",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/attachment-floating/cold/read/hierarchy.xml b/corpus/attachment-floating/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a5c0ebd2bd7e2e4aac8657b09f9dccbb6b78c66f
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/attachment-floating/cold/read/page-000.navigation.json b/corpus/attachment-floating/cold/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..5bc1c19afca5f08217613a447359257f6a310573
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Floating files - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/attachment-floating/cold/read/page-000.png b/corpus/attachment-floating/cold/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..674b8248b0e0343261a903a432992451a88c0d88
Binary files /dev/null and b/corpus/attachment-floating/cold/read/page-000.png differ
diff --git a/corpus/attachment-floating/cold/read/page-000.xml b/corpus/attachment-floating/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c827a48ba1c7d4e8bfb00037bc85a794823afda7
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/attachment-floating/cold/read/payloads.json b/corpus/attachment-floating/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..804d83602a0ca10ae32a46c7f44407b2a81798a2
--- /dev/null
+++ b/corpus/attachment-floating/cold/read/payloads.json
@@ -0,0 +1,18 @@
+[
+ {
+ "sha256": "1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a",
+ "name": "large.bin",
+ "object": "{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{4}{B0}",
+ "kind": "InsertedFile",
+ "page": "{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}",
+ "bytes": 2097152
+ },
+ {
+ "sha256": "78b7cac526a295294b0a4f001cfdc3a8a30ea5dad8d18cc53022a6a5285ca58f",
+ "name": "float 🦀.txt",
+ "object": "{01634379-E6DF-0FE9-1F9E-D863E5BCE12A}{2}{B0}",
+ "kind": "InsertedFile",
+ "page": "{6A838D53-0497-0161-3E0F-95FC73741A4D}{1}{B0}",
+ "bytes": 37
+ }
+]
\ No newline at end of file
diff --git a/corpus/attachment-floating/cold/run.json b/corpus/attachment-floating/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..b790e1bf29e935a2dc7a48d6fa4222fc44c16b79
--- /dev/null
+++ b/corpus/attachment-floating/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-attachments/export",
+ "expected_pages": 1,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "22bc760f955ef59bb5f0ccfd55271f8d868674084127fbe219e1048bd3777f35"
+ }
+}
diff --git a/corpus/attachment-floating/cold/scripts/cold.ps1 b/corpus/attachment-floating/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/attachment-floating/cold/scripts/cold.ps1
@@ -0,0 +1,27 @@
+param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
+$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
+if ($CloneHost) {
+ if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
+ throw 'The disposable clone hostname does not match this machine.'
+ }
+ New-Item "$key\Options\Paths" -Force | Out-Null
+ New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
+} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
+ -not (Test-Path 'C:\one-tests\profile-original-cache')) {
+ throw 'Park the personal OneNote profile before resetting the test cache.'
+}
+$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
+$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
+if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
+New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
+if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
+if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
+New-Item "$key\OpenNotebooks" | Out-Null
+New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
diff --git a/corpus/attachment-floating/cold/scripts/read.ps1 b/corpus/attachment-floating/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a42366b32e8baaf13dae92e9472cae7a071220cc
--- /dev/null
+++ b/corpus/attachment-floating/cold/scripts/read.ps1
@@ -0,0 +1,144 @@
+param(
+ [Parameter(Mandatory=$true)][string]$Root,
+ [int]$ExpectedPages = -1,
+ [switch]$UseCurrentCache,
+ [switch]$Pdf,
+ [switch]$KeepOpen,
+ [string]$CloneHost = ''
+)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+$notebook = Join-Path $root 'notebook'
+$output = Join-Path $root 'read'
+if (Test-Path $output) { throw 'Choose a new read destination.' }
+if ($UseCurrentCache) {
+ if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
+ throw 'Park the personal OneNote profile before reading test notebooks.'
+ }
+} else {
+ & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
+}
+New-Item -ItemType Directory -Path $output | Out-Null
+$app = New-Object -ComObject OneNote.Application
+$notebookId = ''
+$failure = $null
+try {
+ $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
+ $process = Get-Process ONENOTE
+ @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
+ powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
+ ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
+ $sections = @()
+ foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
+ $id = ''
+ $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
+ $sections += $id
+ }
+ $deadline = [DateTime]::UtcNow.AddSeconds(300)
+ $previous = ''
+ $lastChange = ''
+ $stableSince = [DateTime]::UtcNow
+ $settled = $false
+ do {
+ $pages = @{}
+ foreach ($section in $sections) {
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$xml = $hierarchy
+ foreach ($node in $xml.SelectNodes('//*[@path]')) {
+ if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
+ throw 'OneNote opened a section outside the copied notebook.'
+ }
+ }
+ foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
+ $id = $node.GetAttribute('ID')
+ $content = ''
+ $app.GetPageContent($id, [ref]$content, 1, 1)
+ $pages[$id] = $content
+ }
+ }
+ $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
+ if ($signature -ne $previous) {
+ $lastChange = $previous
+ $previous = $signature
+ $stableSince = [DateTime]::UtcNow
+ }
+ if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
+ ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
+ ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
+ Start-Sleep -Milliseconds 250
+ } while ([DateTime]::UtcNow -lt $deadline)
+ if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
+ [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
+ $index = 0
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ $index++
+ }
+ $hierarchy = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
+ [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
+ throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
+ }
+ $index = 0
+ $payloads = @()
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ if ($Pdf) {
+ $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
+ $app.NavigateTo($id, '', $false)
+ $app.Publish($id, $pdfPath, 3, '')
+ if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
+ }
+ [xml]$page = $pages[$id]
+ foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
+ $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
+ $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
+ [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
+ # A paragraph's file is named by its paragraph, one on the page by itself.
+ $owner = if ($file.ParentNode.LocalName -eq 'Page') { $file } else { $file.ParentNode }
+ $payloads += @{ page = $id; object = $owner.GetAttribute('objectID');
+ kind = $file.LocalName; name = $file.GetAttribute('preferredName');
+ sha256 = $hash; bytes = $bytes.Length }
+ }
+ $index++
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
+ $all = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
+ [xml]$finalTree = $all
+ $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
+ if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
+ throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
+ Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
+} catch {
+ $failure = $_
+ [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ throw
+} finally {
+ try {
+ try {
+ if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
+ if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
+ $app.CloseNotebook($notebookId, $false)
+ }
+ } catch {
+ if ($null -eq $failure) { throw }
+ [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ }
+ } finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ }
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ if (-not $UseCurrentCache -and -not $CloneHost) {
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
+ }
+}
diff --git a/corpus/attachment-floating/cold/source.json b/corpus/attachment-floating/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..2bd44a8b45a520d1c0559c03e0c2a7b34ead6158
--- /dev/null
+++ b/corpus/attachment-floating/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 1604,
+ "sha256": "46e74799daaccb7e37abf1696a1920ce47bb39bd721a2e7e15af8d0f6db7416f",
+ "mtime_ns": 1790706678169228316
+ },
+ {
+ "path": "files.one",
+ "bytes": 2106848,
+ "sha256": "cb063e9a0656899d90d4f95e469d9fec1c72bc259d44f673071d6b03e55ab241",
+ "mtime_ns": 1790706678168913736
+ }
+]
diff --git a/corpus/attachment-floating/cold/teardown.json b/corpus/attachment-floating/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/attachment-floating/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/attachment-floating/native/files.one b/corpus/attachment-floating/native/files.one
new file mode 100644
index 0000000000000000000000000000000000000000..b8880db03e219a89b9becd68744d49320f6c97c3
Binary files /dev/null and b/corpus/attachment-floating/native/files.one differ
diff --git a/corpus/attachment-insert/README.md b/corpus/attachment-insert/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..498f9c717030e13e105a52ec010df04e1f627393
--- /dev/null
+++ b/corpus/attachment-insert/README.md
@@ -0,0 +1,42 @@
+# Attaching files from the editor
+
+`candidate/` is `crates/canvas/tests/attachment_insert.rs`'s section: the editor
+attaches `notes 🦀.txt` (30 bytes, with the icon OneNote stored for a text file)
+at the end of "Before the file", "After the file" is typed where the caret
+lands, and `large.bin` (2 MiB of noise, with the canvas's blank-page icon, as
+a host without system icons stores it) is attached after that. Each step is its
+own edit of the ops the editor recorded, sealed into one appended revision.
+`cold/` is a fresh OneNote 2010 read: both files in place, and the bytes OneNote
+materializes to open them (`read/*.attachment`) hash as written. Opened by
+double click and saved by Save As in a lab clone, both matched too.
+`tools/test_attachment_insert.py` checks the capture without a VM. Regenerate
+with `CANVAS_ATTACHMENT_EXPORT` set to a directory while running the test, then
+`tools/native_runner.py DIR COLD --expected-pages 1 --collect-notebook
+--screenshots`.
+
+`without-icon.png` is OneNote opening a file stored without the icon: it draws
+a broken picture, so the editor always stores one.
+
+## What OneNote 2010 does
+
+Observed in a lab clone on 2026-09-29:
+
+- **Storage.** Every attachment goes into the section's own file data store,
+ whatever its size (1 KiB to 300 MiB, through COM and through Insert > Attach
+ File, on a local folder and on a share). OneNote never wrote a `_onefiles`
+ folder. Identical bytes are stored once per section: a second attachment of
+ the same file, or the same icon, references the first payload. Snowbound
+ stores each attachment's bytes anew, which OneNote reads the same.
+- **Icon.** Beside each file it stores a 32-pixel PNG of the file type's
+ system icon, drawn at 24 points, over the name without its extension.
+- **Attach File and drops.** At the caret, the paragraph splits there: the file
+ follows the text before the caret (or replaces an empty first half) and the
+ text after it follows the file with the caret, as an empty paragraph at the
+ end of one. A drop on text does the same at the drop point, after asking
+ whether to attach a copy (the default) or link to the original. Attached or
+ dropped on blank page, the file becomes a page-level object at that point.
+- **Open.** A double click or Open warns once ("Opening attachments could harm
+ your computer and data") and opens a copy from
+ `%TEMP%\OneNote\14.0\NT\N\` under the file's own name. The context menu
+ offers Open, Save As (in the source's folder, under its name), Insert as
+ Printout, Open Original and Copy Link to Original.
diff --git a/corpus/attachment-insert/candidate/Open Notebook.onetoc2 b/corpus/attachment-insert/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..f7d1fb8af7c6af07f21b48f9f775e6db0e1d02f6
Binary files /dev/null and b/corpus/attachment-insert/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/attachment-insert/candidate/files.one b/corpus/attachment-insert/candidate/files.one
new file mode 100644
index 0000000000000000000000000000000000000000..e8af42e5676f6829fc91e6738065fc1c25d9aa7c
Binary files /dev/null and b/corpus/attachment-insert/candidate/files.one differ
diff --git a/corpus/attachment-insert/cold/commands.jsonl b/corpus/attachment-insert/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..616c2bf78e4841cc56ebd6a962b1a8edc018610c
--- /dev/null
+++ b/corpus/attachment-insert/cold/commands.jsonl
@@ -0,0 +1,3 @@
+{"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}
+{"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}
+{"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}
diff --git a/corpus/attachment-insert/cold/machine.json b/corpus/attachment-insert/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..f6fc1e3eabcd10ca9a742fcc49c75e26fb1db08f
--- /dev/null
+++ b/corpus/attachment-insert/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-ddcbcdbf", "hostname": "ONE-M6-DDCBCDBF"}
diff --git a/corpus/attachment-insert/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment b/corpus/attachment-insert/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment
new file mode 100644
index 0000000000000000000000000000000000000000..8e1f70cf2640fa404f4399f624f84db6853af846
Binary files /dev/null and b/corpus/attachment-insert/cold/read/1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a.attachment differ
diff --git a/corpus/attachment-insert/cold/read/6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3.attachment b/corpus/attachment-insert/cold/read/6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3.attachment
new file mode 100644
index 0000000000000000000000000000000000000000..3ff7d0c64c0a009d923f16fd1c9f849ce53e7a05
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3.attachment
@@ -0,0 +1 @@
+Snowbound attached this file.
diff --git a/corpus/attachment-insert/cold/read/environment.json b/corpus/attachment-insert/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..6b59d517e90eae7f066f471698c20d89d9d8fb54
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-DDCBCDBF",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/attachment-insert/cold/read/hierarchy.xml b/corpus/attachment-insert/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0da73567126737e7cd47d9b9fbf8ce590785e396
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/attachment-insert/cold/read/page-000.navigation.json b/corpus/attachment-insert/cold/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..b2cc9630cdb428ace6cdb6faa135abf85f7b4fa0
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Before the file - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/attachment-insert/cold/read/page-000.png b/corpus/attachment-insert/cold/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e5a633f142496932435f09b5acf57ab1a307177
Binary files /dev/null and b/corpus/attachment-insert/cold/read/page-000.png differ
diff --git a/corpus/attachment-insert/cold/read/page-000.xml b/corpus/attachment-insert/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bc87bc2c24125507706e63d3a7760d8bb117829a
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/attachment-insert/cold/read/payloads.json b/corpus/attachment-insert/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..4568f4a4595543ea922e7d74630d74fd96a2e45d
--- /dev/null
+++ b/corpus/attachment-insert/cold/read/payloads.json
@@ -0,0 +1,18 @@
+[
+ {
+ "sha256": "6762b4d4a7d68d79ade02f878b9c64dbce0b0d4830d7ef6dd36d932339bb8ed3",
+ "name": "notes 🦀.txt",
+ "object": "{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{5}{B0}",
+ "kind": "InsertedFile",
+ "page": "{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}",
+ "bytes": 30
+ },
+ {
+ "sha256": "1dec2d4ce43ad4fb609d5ec604ed9cc5a602f5930c9726e553dbcf5b4d51ee3a",
+ "name": "large.bin",
+ "object": "{0E43FDD7-2BBF-0D36-2F38-3F49329E11AD}{8}{B0}",
+ "kind": "InsertedFile",
+ "page": "{7AC920BC-0677-0C27-1640-BEFF46EEC119}{1}{B0}",
+ "bytes": 2097152
+ }
+]
\ No newline at end of file
diff --git a/corpus/attachment-insert/cold/run.json b/corpus/attachment-insert/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..580ee3a030b830081d0d7a707bedde99b3fb8db6
--- /dev/null
+++ b/corpus/attachment-insert/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-attachments/candidate",
+ "expected_pages": 1,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/attachment-insert/cold/scripts/cold.ps1 b/corpus/attachment-insert/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/attachment-insert/cold/scripts/cold.ps1
@@ -0,0 +1,27 @@
+param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
+$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
+if ($CloneHost) {
+ if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
+ throw 'The disposable clone hostname does not match this machine.'
+ }
+ New-Item "$key\Options\Paths" -Force | Out-Null
+ New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
+} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
+ -not (Test-Path 'C:\one-tests\profile-original-cache')) {
+ throw 'Park the personal OneNote profile before resetting the test cache.'
+}
+$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
+$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
+if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
+New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
+if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
+if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
+New-Item "$key\OpenNotebooks" | Out-Null
+New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
diff --git a/corpus/attachment-insert/cold/scripts/read.ps1 b/corpus/attachment-insert/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/attachment-insert/cold/scripts/read.ps1
@@ -0,0 +1,142 @@
+param(
+ [Parameter(Mandatory=$true)][string]$Root,
+ [int]$ExpectedPages = -1,
+ [switch]$UseCurrentCache,
+ [switch]$Pdf,
+ [switch]$KeepOpen,
+ [string]$CloneHost = ''
+)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+$notebook = Join-Path $root 'notebook'
+$output = Join-Path $root 'read'
+if (Test-Path $output) { throw 'Choose a new read destination.' }
+if ($UseCurrentCache) {
+ if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
+ throw 'Park the personal OneNote profile before reading test notebooks.'
+ }
+} else {
+ & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
+}
+New-Item -ItemType Directory -Path $output | Out-Null
+$app = New-Object -ComObject OneNote.Application
+$notebookId = ''
+$failure = $null
+try {
+ $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
+ $process = Get-Process ONENOTE
+ @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
+ powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
+ ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
+ $sections = @()
+ foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
+ $id = ''
+ $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
+ $sections += $id
+ }
+ $deadline = [DateTime]::UtcNow.AddSeconds(300)
+ $previous = ''
+ $lastChange = ''
+ $stableSince = [DateTime]::UtcNow
+ $settled = $false
+ do {
+ $pages = @{}
+ foreach ($section in $sections) {
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$xml = $hierarchy
+ foreach ($node in $xml.SelectNodes('//*[@path]')) {
+ if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
+ throw 'OneNote opened a section outside the copied notebook.'
+ }
+ }
+ foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
+ $id = $node.GetAttribute('ID')
+ $content = ''
+ $app.GetPageContent($id, [ref]$content, 1, 1)
+ $pages[$id] = $content
+ }
+ }
+ $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
+ if ($signature -ne $previous) {
+ $lastChange = $previous
+ $previous = $signature
+ $stableSince = [DateTime]::UtcNow
+ }
+ if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
+ ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
+ ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
+ Start-Sleep -Milliseconds 250
+ } while ([DateTime]::UtcNow -lt $deadline)
+ if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
+ [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
+ $index = 0
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ $index++
+ }
+ $hierarchy = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
+ [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
+ throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
+ }
+ $index = 0
+ $payloads = @()
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ if ($Pdf) {
+ $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
+ $app.NavigateTo($id, '', $false)
+ $app.Publish($id, $pdfPath, 3, '')
+ if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
+ }
+ [xml]$page = $pages[$id]
+ foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
+ $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
+ $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
+ [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
+ $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
+ kind = $file.LocalName; name = $file.GetAttribute('preferredName');
+ sha256 = $hash; bytes = $bytes.Length }
+ }
+ $index++
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
+ $all = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
+ [xml]$finalTree = $all
+ $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
+ if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
+ throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
+ Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
+} catch {
+ $failure = $_
+ [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ throw
+} finally {
+ try {
+ try {
+ if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
+ if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
+ $app.CloseNotebook($notebookId, $false)
+ }
+ } catch {
+ if ($null -eq $failure) { throw }
+ [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ }
+ } finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ }
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ if (-not $UseCurrentCache -and -not $CloneHost) {
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
+ }
+}
diff --git a/corpus/attachment-insert/cold/source.json b/corpus/attachment-insert/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..56347bc632c8449999180c6f0354b23beb1342b0
--- /dev/null
+++ b/corpus/attachment-insert/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 1604,
+ "sha256": "8e2a1137f47d89425a05053b93f0789be77f219ae2bd4d8bb0bdd1b052a93074",
+ "mtime_ns": 1790703504369465697
+ },
+ {
+ "path": "files.one",
+ "bytes": 2108416,
+ "sha256": "3d60c4a8628ec91e1b96c0a4f43391a2e78206ac2d0918dc0c9b3ef8e851c8de",
+ "mtime_ns": 1790703504369204448
+ }
+]
diff --git a/corpus/attachment-insert/cold/teardown.json b/corpus/attachment-insert/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/attachment-insert/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/attachment-insert/without-icon.png b/corpus/attachment-insert/without-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1893322b982c9602dbbb5e4180385d2222234276
Binary files /dev/null and b/corpus/attachment-insert/without-icon.png differ
diff --git a/corpus/custom-tags/README.md b/corpus/custom-tags/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8710cd7036300fc69bfa957e33a63997f74bc96f
--- /dev/null
+++ b/corpus/custom-tags/README.md
@@ -0,0 +1,29 @@
+# Customized note tags
+
+`candidate/` is the editor's output for `custom_tags_store_what_customize_tags_stores`
+in `crates/canvas/tests/custom_tags.rs`: on a section created in Rust, four tags of a
+customized list are applied through `Formatting::Tag`, each definition storing the tag's
+place in the list as its action type: "Snow check" (Green Star, 61, dark red text on Sky
+Blue), "Snow task" (Green Check Box 1, 48, once open and once checked), "Snow circle"
+(Blue Circle 2, 31) and "Snow ink" (no symbol, blue text). Definitions carry the
+NoteTagPropertyStatus bits OneNote's Customize Tags writes: hasLabel, hasFontColor and
+hasHighlightColor where set, hasIcon for a symbol.
+
+`cold/` is a fresh OneNote 2010 read: the four `TagDef` entries keep their type, symbol,
+name and colours, and the tags their completion state. `tools/test_custom_tags.py`
+checks this without a VM. Regenerate with `ONESTORE_CUSTOM_TAGS_EXPORT` set to a new
+absolute directory while running the test, then cold-open it with
+`tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook --screenshots`.
+
+`native/shapes.one` is OneNote 2010's own work in a lab clone:
+
+- "Shapes 1-72" and "Shapes 73-143" tag a paragraph with every NoteTagShape, set through
+ COM (its definitions store NoteTagPropertyStatus 0).
+- "Custom tags" holds a tag made with New Tag (Snow check, as above), applied at the top
+ of the list (action type 0), then after moving it below To Do (a second definition,
+ action type 1), with To Do applied from both places. The UI stores status 15 and 9.
+- "Cells" tags a paragraph `cR-C` with the symbol picked from row R, column C of Modify
+ Tag's symbol gallery, which `crates/snowbound/src/tags.rs` lays out.
+
+OneNote keeps the tag list in `%APPDATA%\Microsoft\OneNote\14.0\Preferences.dat`, written
+when it exits; Snowbound keeps its own in its settings.
diff --git a/corpus/custom-tags/candidate/Open Notebook.onetoc2 b/corpus/custom-tags/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..75410ed4dd0e1ab783b9301179bceb39faa0b2b8
Binary files /dev/null and b/corpus/custom-tags/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/custom-tags/candidate/custom-tags.one b/corpus/custom-tags/candidate/custom-tags.one
new file mode 100644
index 0000000000000000000000000000000000000000..8fb1985f3c4b0cef7de3974b1a4a5b4d89f46787
Binary files /dev/null and b/corpus/custom-tags/candidate/custom-tags.one differ
diff --git a/corpus/custom-tags/cold/commands.jsonl b/corpus/custom-tags/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..39a0043a0ea6cf92b7ee8fbc3a95fc72f54392ba
--- /dev/null
+++ b/corpus/custom-tags/cold/commands.jsonl
@@ -0,0 +1,3 @@
+{"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}
+{"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}
+{"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}
diff --git a/corpus/custom-tags/cold/machine.json b/corpus/custom-tags/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..ed28841e4e3bfa0d812ba6fa3001eea6686eafcc
--- /dev/null
+++ b/corpus/custom-tags/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-defae25b", "hostname": "ONE-M6-DEFAE25B"}
diff --git a/corpus/custom-tags/cold/notebook/Open Notebook.onetoc2 b/corpus/custom-tags/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..9107b84ed5024f65d19534bf110923d5f8f105a6
Binary files /dev/null and b/corpus/custom-tags/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/custom-tags/cold/notebook/custom-tags.one b/corpus/custom-tags/cold/notebook/custom-tags.one
new file mode 100644
index 0000000000000000000000000000000000000000..a4df65945f934434be9ba7ba2eaf32e6a0821343
Binary files /dev/null and b/corpus/custom-tags/cold/notebook/custom-tags.one differ
diff --git a/corpus/custom-tags/cold/read/environment.json b/corpus/custom-tags/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..3085c200ddf4808428ffb90de50939445964b7de
--- /dev/null
+++ b/corpus/custom-tags/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-DEFAE25B",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/custom-tags/cold/read/hierarchy.xml b/corpus/custom-tags/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3a70431bdac77cf400e7b9fc8f6641d05f94b466
--- /dev/null
+++ b/corpus/custom-tags/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/custom-tags/cold/read/page-000.navigation.json b/corpus/custom-tags/cold/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..29097c05d706d0c86e38de57994d67f0a12bf136
--- /dev/null
+++ b/corpus/custom-tags/cold/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Custom symbol and colours - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/custom-tags/cold/read/page-000.png b/corpus/custom-tags/cold/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..73b2c1c59ed8797e84fa670eb69f1b40e86ac61d
Binary files /dev/null and b/corpus/custom-tags/cold/read/page-000.png differ
diff --git a/corpus/custom-tags/cold/read/page-000.xml b/corpus/custom-tags/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a58d4d2c6b165c82c620388c341a349d7f8920e6
--- /dev/null
+++ b/corpus/custom-tags/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/custom-tags/cold/read/payloads.json b/corpus/custom-tags/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/custom-tags/cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/custom-tags/cold/run.json b/corpus/custom-tags/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ac24489b6c6a12af0fa37ff2bb458acf2ed882e
--- /dev/null
+++ b/corpus/custom-tags/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-custom-tags/candidate",
+ "expected_pages": 1,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/custom-tags/cold/scripts/cold.ps1 b/corpus/custom-tags/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/custom-tags/cold/scripts/cold.ps1
@@ -0,0 +1,27 @@
+param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
+$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
+if ($CloneHost) {
+ if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
+ throw 'The disposable clone hostname does not match this machine.'
+ }
+ New-Item "$key\Options\Paths" -Force | Out-Null
+ New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
+} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
+ -not (Test-Path 'C:\one-tests\profile-original-cache')) {
+ throw 'Park the personal OneNote profile before resetting the test cache.'
+}
+$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
+$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
+if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
+New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
+if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
+if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
+New-Item "$key\OpenNotebooks" | Out-Null
+New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
diff --git a/corpus/custom-tags/cold/scripts/read.ps1 b/corpus/custom-tags/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/custom-tags/cold/scripts/read.ps1
@@ -0,0 +1,142 @@
+param(
+ [Parameter(Mandatory=$true)][string]$Root,
+ [int]$ExpectedPages = -1,
+ [switch]$UseCurrentCache,
+ [switch]$Pdf,
+ [switch]$KeepOpen,
+ [string]$CloneHost = ''
+)
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
+if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
+ throw 'Choose a run directly below C:\one-tests\runs.'
+}
+$notebook = Join-Path $root 'notebook'
+$output = Join-Path $root 'read'
+if (Test-Path $output) { throw 'Choose a new read destination.' }
+if ($UseCurrentCache) {
+ if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
+ throw 'Park the personal OneNote profile before reading test notebooks.'
+ }
+} else {
+ & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
+}
+New-Item -ItemType Directory -Path $output | Out-Null
+$app = New-Object -ComObject OneNote.Application
+$notebookId = ''
+$failure = $null
+try {
+ $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
+ $process = Get-Process ONENOTE
+ @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
+ powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
+ ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
+ $sections = @()
+ foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
+ $id = ''
+ $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
+ $sections += $id
+ }
+ $deadline = [DateTime]::UtcNow.AddSeconds(300)
+ $previous = ''
+ $lastChange = ''
+ $stableSince = [DateTime]::UtcNow
+ $settled = $false
+ do {
+ $pages = @{}
+ foreach ($section in $sections) {
+ $hierarchy = ''
+ $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
+ [xml]$xml = $hierarchy
+ foreach ($node in $xml.SelectNodes('//*[@path]')) {
+ if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
+ throw 'OneNote opened a section outside the copied notebook.'
+ }
+ }
+ foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
+ $id = $node.GetAttribute('ID')
+ $content = ''
+ $app.GetPageContent($id, [ref]$content, 1, 1)
+ $pages[$id] = $content
+ }
+ }
+ $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
+ if ($signature -ne $previous) {
+ $lastChange = $previous
+ $previous = $signature
+ $stableSince = [DateTime]::UtcNow
+ }
+ if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
+ ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
+ ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
+ Start-Sleep -Milliseconds 250
+ } while ([DateTime]::UtcNow -lt $deadline)
+ if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
+ [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
+ $index = 0
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ $index++
+ }
+ $hierarchy = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
+ [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
+ throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
+ }
+ $index = 0
+ $payloads = @()
+ foreach ($id in @($pages.Keys | Sort-Object)) {
+ [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
+ if ($Pdf) {
+ $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
+ $app.NavigateTo($id, '', $false)
+ $app.Publish($id, $pdfPath, 3, '')
+ if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
+ }
+ [xml]$page = $pages[$id]
+ foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
+ $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
+ $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
+ [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
+ $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
+ kind = $file.LocalName; name = $file.GetAttribute('preferredName');
+ sha256 = $hash; bytes = $bytes.Length }
+ }
+ $index++
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
+ $all = ''
+ $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
+ [xml]$finalTree = $all
+ $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
+ if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
+ throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
+ }
+ [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
+ Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
+} catch {
+ $failure = $_
+ [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ throw
+} finally {
+ try {
+ try {
+ if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
+ if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
+ $app.CloseNotebook($notebookId, $false)
+ }
+ } catch {
+ if ($null -eq $failure) { throw }
+ [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
+ }
+ } finally {
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ }
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+ if (-not $UseCurrentCache -and -not $CloneHost) {
+ Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
+ }
+}
diff --git a/corpus/custom-tags/cold/source.json b/corpus/custom-tags/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..7bc7590ba024c289f9fa3fce0f789406b3db276f
--- /dev/null
+++ b/corpus/custom-tags/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 1612,
+ "sha256": "2a6009814bda4756e9d474b02433f075adf6071d7cc0650da8502e46519dea1c",
+ "mtime_ns": 1790704660620140274
+ },
+ {
+ "path": "custom-tags.one",
+ "bytes": 8296,
+ "sha256": "91b1b0c54326b6e686b467e74fe51e080b8bd748c4d62f328072c3ace2a3c226",
+ "mtime_ns": 1790704660619982358
+ }
+]
diff --git a/corpus/custom-tags/cold/teardown.json b/corpus/custom-tags/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/custom-tags/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/custom-tags/native/shapes.one b/corpus/custom-tags/native/shapes.one
new file mode 100644
index 0000000000000000000000000000000000000000..b1934259ba37839633301110213d244b4b887d44
Binary files /dev/null and b/corpus/custom-tags/native/shapes.one differ
diff --git a/corpus/page-background/README.md b/corpus/page-background/README.md
index ebf4088f7a77eee2effcad6dea6e302366d59088..8c1d3346f9484bb5156da16935f9b2c97914752b 100644
--- a/corpus/page-background/README.md
+++ b/corpus/page-background/README.md
@@ -2,12 +2,16 @@
`candidate` is `backgrounds_go_on_and_off_pages_that_have_content` in
`crates/snowbound/src/manage.rs` (`SNOWBOUND_BACKGROUND_EXPORT`), on a copy of
-`rule-lines/native/notebook`: the Page Color menu's ops give the ruled "Standard" page
-Ivy's art (`manage::art_ops`), give "Wide" the art and take it away again, and colour
-"SmallGrid" `0x00c8d8e8`, a COLORREF outside OneNote's menu.
+`rule-lines/native/notebook`: the Page Color menu's steps through the editor
+(`CanvasEditor::set_paper`), each stored as one edit. "Standard" takes Ivy's art; "Wide"
+takes it and has it taken away; "College" takes it and undoes; "MediumGrid" takes it,
+undoes and redoes; "LargeGrid" takes Ivy, then Tulips in its place, and undoes; "SmallGrid"
+is coloured `0x00c8d8e8`, a COLORREF outside OneNote's menu; "VeryLargeGrid" is coloured
+Lemon and takes Sparks' art.
-`cold` is its fresh OneNote 2010 read: "Standard" keeps its rule lines under one
-background image (`backgroundImage="true"`), "Wide" keeps its lines and no image, and
-"SmallGrid" reads back `color="#E8D8C8"` and shows it under its grid. `read/page-006.png`
-shows OneNote drawing the art over the rule lines. `tools/test_page_background.py`
-checks this without a VM.
+`cold` is its fresh OneNote 2010 read: every ruled page keeps its lines; "Standard",
+"MediumGrid" and "LargeGrid" show one Ivy background image (`backgroundImage="true"`),
+"Wide" and "College" none; "SmallGrid" reads back `color="#E8D8C8"` under its grid, and
+"VeryLargeGrid" `#FDFDDD` under Sparks. `read/page-003.png` shows OneNote's opaque art
+hiding the rule lines, and `read/page-005.png` its white hiding the Lemon page colour.
+`tools/test_page_background.py` checks this without a VM.
diff --git a/corpus/page-background/candidate/Rules.one b/corpus/page-background/candidate/Rules.one
index b3cac5439567e52d91afb41e0aea8bfe756f673d..f5cf24f04c8978e6c017ab599fc405a94d2ac0db 100644
Binary files a/corpus/page-background/candidate/Rules.one and b/corpus/page-background/candidate/Rules.one differ
diff --git a/corpus/page-background/cold/commands.jsonl b/corpus/page-background/cold/commands.jsonl
index 078755456d9b165dd24331a220e48eef3cdfdbea..68e73b20aceedb4e263f8a939cddf07fe9aa5d0a 100644
--- a/corpus/page-background/cold/commands.jsonl
+++ b/corpus/page-background/cold/commands.jsonl
@@ -1,3 +1,3 @@
{"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}
-{"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}
+{"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}
{"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}
diff --git a/corpus/page-background/cold/machine.json b/corpus/page-background/cold/machine.json
index e58b873f2d473eca43130194d68fecdd79deb49b..d27bf23f1daec1f4a13dbb468ddfae0dfc9aef6b 100644
--- a/corpus/page-background/cold/machine.json
+++ b/corpus/page-background/cold/machine.json
@@ -1 +1 @@
-{"name": "m6-5ef70348", "hostname": "ONE-M6-5EF70348"}
+{"name": "m6-0ea92ebc", "hostname": "ONE-M6-0EA92EBC"}
diff --git a/corpus/page-background/cold/read/environment.json b/corpus/page-background/cold/read/environment.json
index fccbb69c546565860b9d41302c324d6405a6f1fd..f2721d580a8388144caa836b7781533fd2b1fd6a 100644
--- a/corpus/page-background/cold/read/environment.json
+++ b/corpus/page-background/cold/read/environment.json
@@ -1,7 +1,7 @@
{
"powershell": "5.1.14409.1005",
"schema": "xs2010",
- "hostname": "ONE-M6-5EF70348",
+ "hostname": "ONE-M6-0EA92EBC",
"cold": true,
"onenote": "14.0.4763.1000"
}
diff --git a/corpus/page-background/cold/read/hierarchy.xml b/corpus/page-background/cold/read/hierarchy.xml
index 2480abbaf9351670fbea32ca881eda9ec2b47ce7..121d13d9eb8bfb9c9fa19d7805dd0405878f551d 100644
--- a/corpus/page-background/cold/read/hierarchy.xml
+++ b/corpus/page-background/cold/read/hierarchy.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-000.navigation.json b/corpus/page-background/cold/read/page-000.navigation.json
index 7824d62a06fb14853ccd8fca71946931cbe8e9ec..3cda85f0889301dcc34bd7f3046be58554002323 100644
--- a/corpus/page-background/cold/read/page-000.navigation.json
+++ b/corpus/page-background/cold/read/page-000.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "None - Microsoft OneNote",
+ "title": "SmallGrid - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-000.png b/corpus/page-background/cold/read/page-000.png
index 6d53a16941a97c24ef8e89388659739401287191..a53ce7f39242c5e8200b518dc04a34136cf30475 100644
Binary files a/corpus/page-background/cold/read/page-000.png and b/corpus/page-background/cold/read/page-000.png differ
diff --git a/corpus/page-background/cold/read/page-000.xml b/corpus/page-background/cold/read/page-000.xml
index 12cf428804b54db3214d4d440d9a26a08090c293..c13920e172821bda7a9bd874863f2311a42f9683 100644
--- a/corpus/page-background/cold/read/page-000.xml
+++ b/corpus/page-background/cold/read/page-000.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-001.navigation.json b/corpus/page-background/cold/read/page-001.navigation.json
index 1d051ab18621c4450d424efbbd3056b504248b1b..5c585338bca1397dd69f5878808de906cd5ff0ae 100644
--- a/corpus/page-background/cold/read/page-001.navigation.json
+++ b/corpus/page-background/cold/read/page-001.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "College - Microsoft OneNote",
+ "title": "Hidden - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-001.png b/corpus/page-background/cold/read/page-001.png
index 86f49f9425b6b98a8ed88aa8c1675759632059e3..25437d316cbd03ff3c85b33cc4ebe07f1105d9d5 100644
Binary files a/corpus/page-background/cold/read/page-001.png and b/corpus/page-background/cold/read/page-001.png differ
diff --git a/corpus/page-background/cold/read/page-001.xml b/corpus/page-background/cold/read/page-001.xml
index 3578db1b2139354625912e9473a87ebbed2eff28..dd8b252fedfd711c1df59170e7106729a813c1d7 100644
--- a/corpus/page-background/cold/read/page-001.xml
+++ b/corpus/page-background/cold/read/page-001.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-002.navigation.json b/corpus/page-background/cold/read/page-002.navigation.json
index 909744712d947f9080464d99bb701cd7de4071c8..6ea1c2e7cfb4aec222874c087cbbaf51b80cb3c7 100644
--- a/corpus/page-background/cold/read/page-002.navigation.json
+++ b/corpus/page-background/cold/read/page-002.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "VeryLargeGrid - Microsoft OneNote",
+ "title": "LargeGrid - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-002.png b/corpus/page-background/cold/read/page-002.png
index 75a53d42b8eb63e08cdc6d28336d940273b7e2f4..6747da8bcc5b530c3a932fe3e7ce1ee8720981d5 100644
Binary files a/corpus/page-background/cold/read/page-002.png and b/corpus/page-background/cold/read/page-002.png differ
diff --git a/corpus/page-background/cold/read/page-002.xml b/corpus/page-background/cold/read/page-002.xml
index 7b7b9f465790ce5737d5f211006ff058494757be..afd45de00c93ebb8d8e1f073af589826f9dcb499 100644
--- a/corpus/page-background/cold/read/page-002.xml
+++ b/corpus/page-background/cold/read/page-002.xml
@@ -1,2 +1,353 @@
-
+/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB
+EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL
+EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU
+FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID
+BAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHB
+FVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2
+d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna
+4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1
+EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ0
+4SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeI
+iYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery
+8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAOS8f+KbvwbFYamkX2nThL5V3EB8wDfdcH2wRjoc1
+5GYYqpg+SqleN7NfkzGtV9jDnauup0el6nbazp8F7aSCW2nUOjjuK9KlVhXgqkHdM1TUldbHK/Cf
+/kW7r/sIXH/odeblf8F/4pfmcGB/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAB
+QAUAFABQAUAFABQBW1HTrbVrGazu4lntpl2PG3QisqtKFaDpzV0xNKSszmNP0uP4X+ENUZLh7y1t
+vMuIUkGCgxwme/PfjrXlU6X9l4ao+bmSu1/l95lCEcPTstlf/Mz/AIM3Dt4GM03+ta6mZsjHzFuf
+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
diff --git a/corpus/page-background/cold/read/page-003.navigation.json b/corpus/page-background/cold/read/page-003.navigation.json
index 7ccafd7fd59c95368f51d575f411c3fb815d3863..74f74505c909f96ff84679b418cd721b7be7459f 100644
--- a/corpus/page-background/cold/read/page-003.navigation.json
+++ b/corpus/page-background/cold/read/page-003.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "MediumGrid - Microsoft OneNote",
+ "title": "Standard - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-003.png b/corpus/page-background/cold/read/page-003.png
index 4adfda1220e6a91319553962aa0b842b9c535abe..f5590c79315f620da923111dd269c02a8fd9e42b 100644
Binary files a/corpus/page-background/cold/read/page-003.png and b/corpus/page-background/cold/read/page-003.png differ
diff --git a/corpus/page-background/cold/read/page-003.xml b/corpus/page-background/cold/read/page-003.xml
index 3544c37d82f5ce4bbe4d70bdc5ac292060eed29b..4afa95c1595214cd2f572acc2051a4d3eb3969e4 100644
--- a/corpus/page-background/cold/read/page-003.xml
+++ b/corpus/page-background/cold/read/page-003.xml
@@ -1,2 +1,353 @@
-
+/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB
+EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL
+EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU
+FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID
+BAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHB
+FVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2
+d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna
+4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1
+EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ0
+4SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeI
+iYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery
+8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAOS8f+KbvwbFYamkX2nThL5V3EB8wDfdcH2wRjoc1
+5GYYqpg+SqleN7NfkzGtV9jDnauup0el6nbazp8F7aSCW2nUOjjuK9KlVhXgqkHdM1TUldbHK/Cf
+/kW7r/sIXH/odeblf8F/4pfmcGB/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAB
+QAUAFABQAUAFABQBW1HTrbVrGazu4lntpl2PG3QisqtKFaDpzV0xNKSszmNP0uP4X+ENUZLh7y1t
+vMuIUkGCgxwme/PfjrXlU6X9l4ao+bmSu1/l95lCEcPTstlf/Mz/AIM3Dt4GM03+ta6mZsjHzFuf
+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
diff --git a/corpus/page-background/cold/read/page-004.navigation.json b/corpus/page-background/cold/read/page-004.navigation.json
index 007050b6d7a6917406f9ab04289d337fab5d8d43..d2a29b6a0602e35842b0605919ee79aa85216a62 100644
--- a/corpus/page-background/cold/read/page-004.navigation.json
+++ b/corpus/page-background/cold/read/page-004.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "Wide - Microsoft OneNote",
+ "title": "ColorRed - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-004.png b/corpus/page-background/cold/read/page-004.png
index d70d67d33fff88c810540c9cc6048c0da36b0d47..e6c86006d290b84f16ec3ab6ebde2170313d7a3d 100644
Binary files a/corpus/page-background/cold/read/page-004.png and b/corpus/page-background/cold/read/page-004.png differ
diff --git a/corpus/page-background/cold/read/page-004.xml b/corpus/page-background/cold/read/page-004.xml
index cd5eb9a8d6fd489b97c26548fb5c08c58b3d5e71..1de9b7b4dab480874c9f1bc841871590d95540c7 100644
--- a/corpus/page-background/cold/read/page-004.xml
+++ b/corpus/page-background/cold/read/page-004.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-005.navigation.json b/corpus/page-background/cold/read/page-005.navigation.json
index 7f25097d7917248dd57c3d10c5466fadaaa84a95..909744712d947f9080464d99bb701cd7de4071c8 100644
--- a/corpus/page-background/cold/read/page-005.navigation.json
+++ b/corpus/page-background/cold/read/page-005.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "Narrow - Microsoft OneNote",
+ "title": "VeryLargeGrid - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-005.png b/corpus/page-background/cold/read/page-005.png
index 0ff75c9f29678987fa5562d0c36b6d9438c63975..1524b8667172edf877b7149a3338fb2437a967cd 100644
Binary files a/corpus/page-background/cold/read/page-005.png and b/corpus/page-background/cold/read/page-005.png differ
diff --git a/corpus/page-background/cold/read/page-005.xml b/corpus/page-background/cold/read/page-005.xml
index a50c40d773c0bc67794d26ec2a6f57a39c985c87..3191154b5de39b0109296ef1222a3ab2cd5e88cb 100644
--- a/corpus/page-background/cold/read/page-005.xml
+++ b/corpus/page-background/cold/read/page-005.xml
@@ -1,2 +1,770 @@
-
+/9j/4AAQSkZJRgABAgAAAQABAAD//gAbU25vd2JvdW5kIHRlbXBsYXRlIHNwYXJrc//AABEIAxgC
+ZAMBEQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4R
+DgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQU
+FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAA
+AQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgj
+QrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpz
+dHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX
+2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/
+xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEK
+FiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SF
+hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo
+6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAPLNc/wCSlTf8B/8ARIr8OzL/AJKqfy/9No+4
+w3/IqX9faNivpjzAoAKpAOXpTAWmgCqAVaCWLTQBTAKCWOXpVIQo60wH0AFUgLEC/aIjH/GnzL7j
+uK66cfawcOq1X6oyk+R36EaJv3ewzWUI81/ItuxJbw+YxZv9Wgyx/pWlKnzu8tluTOXLot2Ru5kc
+se/as5S5m2xpcqsC9KSAWmAUCYVSEOHSqQDl61SAWqJY5asQo600QPq0Aq9asTFqkIeOlUQKtUA8
+DjNaWExaaJH1YCjrTQMdVEjh0q0Sxy1QhR1qgH1SIHDpVoTFXrViHVSAfTICrAKACgAoAKACgAoA
+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
diff --git a/corpus/page-background/cold/read/page-006.navigation.json b/corpus/page-background/cold/read/page-006.navigation.json
index 74f74505c909f96ff84679b418cd721b7be7459f..7ccafd7fd59c95368f51d575f411c3fb815d3863 100644
--- a/corpus/page-background/cold/read/page-006.navigation.json
+++ b/corpus/page-background/cold/read/page-006.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "Standard - Microsoft OneNote",
+ "title": "MediumGrid - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-006.png b/corpus/page-background/cold/read/page-006.png
index b529348ff769e5ea373a48ffec8425339c59d681..927fd3db08d6c6da0a7b70bd6bfa9d2780ece922 100644
Binary files a/corpus/page-background/cold/read/page-006.png and b/corpus/page-background/cold/read/page-006.png differ
diff --git a/corpus/page-background/cold/read/page-006.xml b/corpus/page-background/cold/read/page-006.xml
index 3d7a8ca5f93aef9d28145f04b73701ba123c7855..4b19f1ff1ec8d4fd3b844fa670df4e936bf991cd 100644
--- a/corpus/page-background/cold/read/page-006.xml
+++ b/corpus/page-background/cold/read/page-006.xml
@@ -1,5 +1,5 @@
-/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB
+/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB
EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL
EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU
FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID
diff --git a/corpus/page-background/cold/read/page-007.navigation.json b/corpus/page-background/cold/read/page-007.navigation.json
index 6ea1c2e7cfb4aec222874c087cbbaf51b80cb3c7..007050b6d7a6917406f9ab04289d337fab5d8d43 100644
--- a/corpus/page-background/cold/read/page-007.navigation.json
+++ b/corpus/page-background/cold/read/page-007.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "LargeGrid - Microsoft OneNote",
+ "title": "Wide - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-007.png b/corpus/page-background/cold/read/page-007.png
index 16bcfdc5106ef59235784c52de70b3b2e78111cb..eccce01fe07d242b54e51b56e423d164a3f850de 100644
Binary files a/corpus/page-background/cold/read/page-007.png and b/corpus/page-background/cold/read/page-007.png differ
diff --git a/corpus/page-background/cold/read/page-007.xml b/corpus/page-background/cold/read/page-007.xml
index f6255c7de82987f898fd5b3afa20bdb00fac5dd9..6e726e368339ce3a0112d573c93f56cefca5cb73 100644
--- a/corpus/page-background/cold/read/page-007.xml
+++ b/corpus/page-background/cold/read/page-007.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-008.navigation.json b/corpus/page-background/cold/read/page-008.navigation.json
index d2a29b6a0602e35842b0605919ee79aa85216a62..7f25097d7917248dd57c3d10c5466fadaaa84a95 100644
--- a/corpus/page-background/cold/read/page-008.navigation.json
+++ b/corpus/page-background/cold/read/page-008.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "ColorRed - Microsoft OneNote",
+ "title": "Narrow - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-008.png b/corpus/page-background/cold/read/page-008.png
index 9b309ee670f3838fcab2d007a7549ba9921aeec7..cf7fecdf42989be69d24ec1e43686d4a2b503111 100644
Binary files a/corpus/page-background/cold/read/page-008.png and b/corpus/page-background/cold/read/page-008.png differ
diff --git a/corpus/page-background/cold/read/page-008.xml b/corpus/page-background/cold/read/page-008.xml
index 2d6b4f741bc5f03ec29303610f43fbb8d56de2c1..838f862aecd61c5b71e421991e108eb9d6bd51b4 100644
--- a/corpus/page-background/cold/read/page-008.xml
+++ b/corpus/page-background/cold/read/page-008.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-009.navigation.json b/corpus/page-background/cold/read/page-009.navigation.json
index 3cda85f0889301dcc34bd7f3046be58554002323..7824d62a06fb14853ccd8fca71946931cbe8e9ec 100644
--- a/corpus/page-background/cold/read/page-009.navigation.json
+++ b/corpus/page-background/cold/read/page-009.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "SmallGrid - Microsoft OneNote",
+ "title": "None - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-009.png b/corpus/page-background/cold/read/page-009.png
index 07af1f35fa610f3eb4fb07e679391ccc07846f74..616d089022c06aac2ab76f6e26a11302d690f27f 100644
Binary files a/corpus/page-background/cold/read/page-009.png and b/corpus/page-background/cold/read/page-009.png differ
diff --git a/corpus/page-background/cold/read/page-009.xml b/corpus/page-background/cold/read/page-009.xml
index cdff2258459a79cf71abc3a86be1f028e65dbe19..24ab09edb7d858d5ebe1c36411153bff2723cf38 100644
--- a/corpus/page-background/cold/read/page-009.xml
+++ b/corpus/page-background/cold/read/page-009.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/read/page-010.navigation.json b/corpus/page-background/cold/read/page-010.navigation.json
index 5c585338bca1397dd69f5878808de906cd5ff0ae..1d051ab18621c4450d424efbbd3056b504248b1b 100644
--- a/corpus/page-background/cold/read/page-010.navigation.json
+++ b/corpus/page-background/cold/read/page-010.navigation.json
@@ -6,7 +6,7 @@
"h": 600,
"error": null,
"win": {
- "title": "Hidden - Microsoft OneNote",
+ "title": "College - Microsoft OneNote",
"class": "Framework::CFrame",
"dialog": false
}
diff --git a/corpus/page-background/cold/read/page-010.png b/corpus/page-background/cold/read/page-010.png
index 756322df669e4169f0910aadc989f3608b11c603..3c21de14d9a31497580f2b669ac74cf3582994e0 100644
Binary files a/corpus/page-background/cold/read/page-010.png and b/corpus/page-background/cold/read/page-010.png differ
diff --git a/corpus/page-background/cold/read/page-010.xml b/corpus/page-background/cold/read/page-010.xml
index b22513a98ff771e9450f944bcccc1dc1a187e55e..9c2c179ce5a53d907f1ee0844314d30a44d234fb 100644
--- a/corpus/page-background/cold/read/page-010.xml
+++ b/corpus/page-background/cold/read/page-010.xml
@@ -1,2 +1,2 @@
-
+
diff --git a/corpus/page-background/cold/run.json b/corpus/page-background/cold/run.json
index 2e035ab67362f60cf49d3ae690d4fca77cef7237..2562945997444483cf49dca0f763a80f512ae1ca 100644
--- a/corpus/page-background/cold/run.json
+++ b/corpus/page-background/cold/run.json
@@ -1,5 +1,5 @@
{
- "notebook": "/private/tmp/snowbound-page-background/candidate",
+ "notebook": "/private/tmp/snowbound-art-undo/candidate",
"expected_pages": 11,
"author": null,
"author_timeout_seconds": 600,
diff --git a/corpus/page-background/cold/source.json b/corpus/page-background/cold/source.json
index d05a2062f5d643ca05e92a4d82d237e2bc8e322a..1c2d8c51c21790d966f20a319f5703aba24036b1 100644
--- a/corpus/page-background/cold/source.json
+++ b/corpus/page-background/cold/source.json
@@ -7,8 +7,8 @@
},
{
"path": "Rules.one",
- "bytes": 132704,
- "sha256": "a513e56274ae794cf558da34dca7189225159d7eb6ab7d2256531fd7a2a55b44",
- "mtime_ns": 1790671178905073494
+ "bytes": 324016,
+ "sha256": "53c49a8bd35b4aee2ec840ef79fd7bdb6e6b4bc3cb3885d56cccbac145389a8b",
+ "mtime_ns": 1790701954004992686
}
]
diff --git a/crates/canvas/assets/file.svg b/crates/canvas/assets/file.svg
new file mode 100644
index 0000000000000000000000000000000000000000..54e4203f4383cc0fb954ae638ae8331ec119de9e
--- /dev/null
+++ b/crates/canvas/assets/file.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-down-blue.svg b/crates/canvas/assets/tags/arrow-down-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..21ebf9fdfc55f868d11ea037b993c2c71de789d6
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-down-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-down-green.svg b/crates/canvas/assets/tags/arrow-down-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e60e9600eef247ea407430f9cb0969522d92d217
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-down-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-down-yellow.svg b/crates/canvas/assets/tags/arrow-down-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0b6aea7e4139e56b185d2ceee67433ff78522c89
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-down-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-left-blue.svg b/crates/canvas/assets/tags/arrow-left-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..762f595a16abe28964b176ddb10e5a12f463679c
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-left-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-left-green.svg b/crates/canvas/assets/tags/arrow-left-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..462e7bdd363067db2805444044aff4f17ce58dc1
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-left-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-left-yellow.svg b/crates/canvas/assets/tags/arrow-left-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..131ba5f6f5a1efb8e2833af502fb32ce3c19f3c8
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-left-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-right-blue.svg b/crates/canvas/assets/tags/arrow-right-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2ff8c4ed1446a44d7ad073370e865173eb666eca
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-right-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-right-green.svg b/crates/canvas/assets/tags/arrow-right-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a312022654fca268e4e36f0afa1cef4825356154
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-right-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-right-yellow.svg b/crates/canvas/assets/tags/arrow-right-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5615dd5c417423027d4e17d0672f8b91d5627da9
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-right-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-up-blue.svg b/crates/canvas/assets/tags/arrow-up-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..056f56d291e26dab526d9e931e8622bbdd1f2cfa
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-up-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-up-green.svg b/crates/canvas/assets/tags/arrow-up-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..580b5bb37993ea94f4d066b1b8b9ccd82b36063c
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-up-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/arrow-up-yellow.svg b/crates/canvas/assets/tags/arrow-up-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a17874ae0bbc03a025d095d05808cfcff534ad05
--- /dev/null
+++ b/crates/canvas/assets/tags/arrow-up-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/bell.svg b/crates/canvas/assets/tags/bell.svg
new file mode 100644
index 0000000000000000000000000000000000000000..76e9286f4004e3356ced5df23667252b0ac79742
--- /dev/null
+++ b/crates/canvas/assets/tags/bell.svg
@@ -0,0 +1,15 @@
+
diff --git a/crates/canvas/assets/tags/binoculars.svg b/crates/canvas/assets/tags/binoculars.svg
new file mode 100644
index 0000000000000000000000000000000000000000..20f50a22eaeae62aee1cb3f8be69ec24b7284d15
--- /dev/null
+++ b/crates/canvas/assets/tags/binoculars.svg
@@ -0,0 +1,14 @@
+
diff --git a/crates/canvas/assets/tags/calendar.svg b/crates/canvas/assets/tags/calendar.svg
new file mode 100644
index 0000000000000000000000000000000000000000..36354895fde58d92f49d3187edf5ea84d0bc4f6e
--- /dev/null
+++ b/crates/canvas/assets/tags/calendar.svg
@@ -0,0 +1,25 @@
+
diff --git a/crates/canvas/assets/tags/car.svg b/crates/canvas/assets/tags/car.svg
new file mode 100644
index 0000000000000000000000000000000000000000..27bcc80a7b8de061bbbb6d3f3fa32c0075dd343a
--- /dev/null
+++ b/crates/canvas/assets/tags/car.svg
@@ -0,0 +1,13 @@
+
diff --git a/crates/canvas/assets/tags/checkbox-green.svg b/crates/canvas/assets/tags/checkbox-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..26a689bc0d346500fb20cddcb55bb7b8cfda1d45
--- /dev/null
+++ b/crates/canvas/assets/tags/checkbox-green.svg
@@ -0,0 +1,24 @@
+
diff --git a/crates/canvas/assets/tags/checkbox-yellow.svg b/crates/canvas/assets/tags/checkbox-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..20d79b4f4cb3c19d23e417529533d4693455260a
--- /dev/null
+++ b/crates/canvas/assets/tags/checkbox-yellow.svg
@@ -0,0 +1,24 @@
+
diff --git a/crates/canvas/assets/tags/circle-1-blue.svg b/crates/canvas/assets/tags/circle-1-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3b5f63b38e352e7bd311b8fc3e3ce5c33f74cada
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-1-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-1-green.svg b/crates/canvas/assets/tags/circle-1-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..787846fa423ed588f789494eb0a2b179769d3f21
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-1-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-1-yellow.svg b/crates/canvas/assets/tags/circle-1-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..618ff6e4a24caedcf0fae04607636339deafa6cb
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-1-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-2-blue.svg b/crates/canvas/assets/tags/circle-2-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9029590157f17586c158aa3fb300e4d15d4c2f8b
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-2-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-2-green.svg b/crates/canvas/assets/tags/circle-2-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..da31358f66626543353d8a75a019b479450f03a7
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-2-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-2-yellow.svg b/crates/canvas/assets/tags/circle-2-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..82022f6fa060589b9200a0f46d2b4aa15eed138b
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-2-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-3-blue.svg b/crates/canvas/assets/tags/circle-3-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b691add4bcca2e9061d8aaeb742f3f54e2c34ab3
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-3-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-3-green.svg b/crates/canvas/assets/tags/circle-3-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f46d22e25695ad9de0ff9cde08e8cc03fda5a4ae
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-3-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-3-yellow.svg b/crates/canvas/assets/tags/circle-3-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..94428e5b10a79357a9f21fcf23678722e4c03a82
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-3-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/circle-blue.svg b/crates/canvas/assets/tags/circle-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1822f9dbf90b0bdfc142f5f13ff2853a540f142b
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-blue.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/circle-green.svg b/crates/canvas/assets/tags/circle-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a2a1d72b1efb6ea10eafd4e1802d82fa099b9e9c
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-green.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/circle-yellow.svg b/crates/canvas/assets/tags/circle-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..81e821e594b583a1eae9bb6dd4520bf964f5e40a
--- /dev/null
+++ b/crates/canvas/assets/tags/circle-yellow.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/clock.svg b/crates/canvas/assets/tags/clock.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e879f70949ce28cf5db576c38b40f2fa38950dc7
--- /dev/null
+++ b/crates/canvas/assets/tags/clock.svg
@@ -0,0 +1,21 @@
+
diff --git a/crates/canvas/assets/tags/cloud.svg b/crates/canvas/assets/tags/cloud.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6d6aef78d7396e120169a832ad6a41f6d4d9f866
--- /dev/null
+++ b/crates/canvas/assets/tags/cloud.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/coins.svg b/crates/canvas/assets/tags/coins.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ac816813a2d67a6e244f9458cf1dc91112c46d82
--- /dev/null
+++ b/crates/canvas/assets/tags/coins.svg
@@ -0,0 +1,30 @@
+
diff --git a/crates/canvas/assets/tags/date.svg b/crates/canvas/assets/tags/date.svg
new file mode 100644
index 0000000000000000000000000000000000000000..aa402454b3a5e9d7e945265593804998917029d8
--- /dev/null
+++ b/crates/canvas/assets/tags/date.svg
@@ -0,0 +1,43 @@
+
diff --git a/crates/canvas/assets/tags/dollar.svg b/crates/canvas/assets/tags/dollar.svg
new file mode 100644
index 0000000000000000000000000000000000000000..04bb22a57765dc7430062ee35af0e81540dcd597
--- /dev/null
+++ b/crates/canvas/assets/tags/dollar.svg
@@ -0,0 +1,5 @@
+
diff --git a/crates/canvas/assets/tags/envelope-open.svg b/crates/canvas/assets/tags/envelope-open.svg
new file mode 100644
index 0000000000000000000000000000000000000000..62a0f78f205fb1a90f2e56b4d4c30a372fb1d96c
--- /dev/null
+++ b/crates/canvas/assets/tags/envelope-open.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/envelope.svg b/crates/canvas/assets/tags/envelope.svg
new file mode 100644
index 0000000000000000000000000000000000000000..18fb355234249db23ccf4bcfde30e3842c92f1e3
--- /dev/null
+++ b/crates/canvas/assets/tags/envelope.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/flag-next-week.svg b/crates/canvas/assets/tags/flag-next-week.svg
new file mode 100644
index 0000000000000000000000000000000000000000..415b077018170c9c0b568cf02a69516d1c0b734a
--- /dev/null
+++ b/crates/canvas/assets/tags/flag-next-week.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/flag-no-date.svg b/crates/canvas/assets/tags/flag-no-date.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9fb50c1ae76f0c1ee9bf023b8f6ec8c806a4e66f
--- /dev/null
+++ b/crates/canvas/assets/tags/flag-no-date.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/flag-this-week.svg b/crates/canvas/assets/tags/flag-this-week.svg
new file mode 100644
index 0000000000000000000000000000000000000000..578c5141d7ab7bae47284b52a8409f24e4b17f98
--- /dev/null
+++ b/crates/canvas/assets/tags/flag-this-week.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/flag-today.svg b/crates/canvas/assets/tags/flag-today.svg
new file mode 100644
index 0000000000000000000000000000000000000000..aa324b20abecc9a758fdad577617b21b19dc40dc
--- /dev/null
+++ b/crates/canvas/assets/tags/flag-today.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/flag-tomorrow.svg b/crates/canvas/assets/tags/flag-tomorrow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3fc2e35252210e4f14a08e678c258ce8469edb28
--- /dev/null
+++ b/crates/canvas/assets/tags/flag-tomorrow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/follow-up.svg b/crates/canvas/assets/tags/follow-up.svg
new file mode 100644
index 0000000000000000000000000000000000000000..54c74592d54526a1159e1e8a1786368a14aa214a
--- /dev/null
+++ b/crates/canvas/assets/tags/follow-up.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/frown.svg b/crates/canvas/assets/tags/frown.svg
new file mode 100644
index 0000000000000000000000000000000000000000..938ef19cddfd3282a5e04885df992d1a73d1dea2
--- /dev/null
+++ b/crates/canvas/assets/tags/frown.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/globe.svg b/crates/canvas/assets/tags/globe.svg
new file mode 100644
index 0000000000000000000000000000000000000000..619c313e389591c96a5839c32a6626260c4afeeb
--- /dev/null
+++ b/crates/canvas/assets/tags/globe.svg
@@ -0,0 +1,12 @@
+
diff --git a/crates/canvas/assets/tags/green-square.svg b/crates/canvas/assets/tags/green-square.svg
new file mode 100644
index 0000000000000000000000000000000000000000..694168a18dee92bff3696962e5f4d61b603183cf
--- /dev/null
+++ b/crates/canvas/assets/tags/green-square.svg
@@ -0,0 +1,19 @@
+
diff --git a/crates/canvas/assets/tags/heart.svg b/crates/canvas/assets/tags/heart.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6ae8f81901b7a2bed80eccf7dc7850c238c6ff5a
--- /dev/null
+++ b/crates/canvas/assets/tags/heart.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/im-contact.svg b/crates/canvas/assets/tags/im-contact.svg
new file mode 100644
index 0000000000000000000000000000000000000000..937f79464b9e3f31c7651074ba82f63ec71dba15
--- /dev/null
+++ b/crates/canvas/assets/tags/im-contact.svg
@@ -0,0 +1,10 @@
+
diff --git a/crates/canvas/assets/tags/key.svg b/crates/canvas/assets/tags/key.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d2fa291a4d63eacf3e3061caf5f0fb189fda362a
--- /dev/null
+++ b/crates/canvas/assets/tags/key.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/laptop.svg b/crates/canvas/assets/tags/laptop.svg
new file mode 100644
index 0000000000000000000000000000000000000000..016059a8a2f969e43639baa85272009009c0f6eb
--- /dev/null
+++ b/crates/canvas/assets/tags/laptop.svg
@@ -0,0 +1,10 @@
+
diff --git a/crates/canvas/assets/tags/lightning.svg b/crates/canvas/assets/tags/lightning.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f55218d3e77540cae3386db244f19c78ed62e564
--- /dev/null
+++ b/crates/canvas/assets/tags/lightning.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-arrow-blue.svg b/crates/canvas/assets/tags/mark-arrow-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..97c2c3c44577ef5dad304ca3244098182964baa4
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-arrow-blue.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/mark-arrow-green.svg b/crates/canvas/assets/tags/mark-arrow-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..512ac40f957d091737b171cb1fc8ca4ea9d435d5
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-arrow-green.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/mark-arrow-yellow.svg b/crates/canvas/assets/tags/mark-arrow-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..34389c3c7493b5cc263937bac07d4e3e4a0372cc
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-arrow-yellow.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/mark-arrow.svg b/crates/canvas/assets/tags/mark-arrow.svg
deleted file mode 100644
index 97c2c3c44577ef5dad304ca3244098182964baa4..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-arrow.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mark-client.svg b/crates/canvas/assets/tags/mark-client.svg
deleted file mode 100644
index b562e9082ec9bf2d0e6f6dad91a46658025f3f36..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-client.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mark-exclamation-blue.svg b/crates/canvas/assets/tags/mark-exclamation-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..de951416c56df76d3df63727c07414ea48fb5bab
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-exclamation-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-exclamation-green.svg b/crates/canvas/assets/tags/mark-exclamation-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..67a7f3799ecb6aea7aefa3536d92e6e5c9cba7f9
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-exclamation-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-exclamation-yellow.svg b/crates/canvas/assets/tags/mark-exclamation-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b562e9082ec9bf2d0e6f6dad91a46658025f3f36
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-exclamation-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-flag-blue.svg b/crates/canvas/assets/tags/mark-flag-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..878943aa2791d75a33791f1ab3d7fe0daf4b7e90
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-flag-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-flag-green.svg b/crates/canvas/assets/tags/mark-flag-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fa9780a1eb2742cfc2cc73232cfbc3dd75f41235
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-flag-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-flag-yellow.svg b/crates/canvas/assets/tags/mark-flag-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7700e494609f2c94eb96cddbfcaa1dcaf960f9b8
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-flag-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-manager.svg b/crates/canvas/assets/tags/mark-manager.svg
deleted file mode 100644
index c7b1b3e74d3f5454151a4bd08edcabb2f54a4dee..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-manager.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mark-one-blue.svg b/crates/canvas/assets/tags/mark-one-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f95d8aa3643bd45b9b71e6c9d4756543526d9c00
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-one-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-one-green.svg b/crates/canvas/assets/tags/mark-one-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e0f1a75f255736d27b2f38c20733cb0deabc2997
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-one-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-one-yellow.svg b/crates/canvas/assets/tags/mark-one-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..218834e755712a51e2d812bded96a093729f0cbe
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-one-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-one.svg b/crates/canvas/assets/tags/mark-one.svg
deleted file mode 100644
index f95d8aa3643bd45b9b71e6c9d4756543526d9c00..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-one.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mark-person-blue.svg b/crates/canvas/assets/tags/mark-person-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1f2d6f0affa1495cf724a9825b126c6d175e5a81
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-person-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-person-green.svg b/crates/canvas/assets/tags/mark-person-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e57d0341831f043771649d26b2d00876f0ae7ada
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-person-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-person-yellow.svg b/crates/canvas/assets/tags/mark-person-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c7b1b3e74d3f5454151a4bd08edcabb2f54a4dee
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-person-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-person.svg b/crates/canvas/assets/tags/mark-person.svg
deleted file mode 100644
index 1f2d6f0affa1495cf724a9825b126c6d175e5a81..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-person.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mark-star-blue.svg b/crates/canvas/assets/tags/mark-star-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..64fc936e96b2dca0a40fc90e64909cbccd1b376c
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-star-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-star-green.svg b/crates/canvas/assets/tags/mark-star-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ab4796db3423d428c90af8be4b51038af88a5119
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-star-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-star-yellow.svg b/crates/canvas/assets/tags/mark-star-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bd1f6c89d96aff646618f66d240d73b103f46cf3
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-star-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-three-blue.svg b/crates/canvas/assets/tags/mark-three-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0e8fb6b92842fa4224379bf207198bafbba11154
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-three-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-three-green.svg b/crates/canvas/assets/tags/mark-three-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..cb2c99e7a2d95c066a73922b046b00f0b6a53861
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-three-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-three-yellow.svg b/crates/canvas/assets/tags/mark-three-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a742086017b7bfab0a90e6fc57b97b065f9e4bab
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-three-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-two-blue.svg b/crates/canvas/assets/tags/mark-two-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..40e030cafc219eb29bd7ee96e2208fa36dc01f32
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-two-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-two-green.svg b/crates/canvas/assets/tags/mark-two-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a7484b31afe5f08b34e55e527546ded2bb5e8620
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-two-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-two-yellow.svg b/crates/canvas/assets/tags/mark-two-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..43c86f79bd98b9127b769d353f8089e1f89d1294
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-two-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-two.svg b/crates/canvas/assets/tags/mark-two.svg
deleted file mode 100644
index 43c86f79bd98b9127b769d353f8089e1f89d1294..0000000000000000000000000000000000000000
--- a/crates/canvas/assets/tags/mark-two.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/crates/canvas/assets/tags/mobile.svg b/crates/canvas/assets/tags/mobile.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c613fa51fd269089eff49101be045f3d93dfbc12
--- /dev/null
+++ b/crates/canvas/assets/tags/mobile.svg
@@ -0,0 +1,19 @@
+
diff --git a/crates/canvas/assets/tags/notebook.svg b/crates/canvas/assets/tags/notebook.svg
new file mode 100644
index 0000000000000000000000000000000000000000..08b1839da0ef41052b3734ea83b655021e686807
--- /dev/null
+++ b/crates/canvas/assets/tags/notebook.svg
@@ -0,0 +1,32 @@
+
diff --git a/crates/canvas/assets/tags/orange-square.svg b/crates/canvas/assets/tags/orange-square.svg
new file mode 100644
index 0000000000000000000000000000000000000000..60db6dbbd4dd9a3a35be73964ebd8d2f507b027e
--- /dev/null
+++ b/crates/canvas/assets/tags/orange-square.svg
@@ -0,0 +1,19 @@
+
diff --git a/crates/canvas/assets/tags/paper.svg b/crates/canvas/assets/tags/paper.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b016c869026b35e9f6d0700b7f2b2c8e4a072f87
--- /dev/null
+++ b/crates/canvas/assets/tags/paper.svg
@@ -0,0 +1,14 @@
+
diff --git a/crates/canvas/assets/tags/paperclip.svg b/crates/canvas/assets/tags/paperclip.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e7868682171f3886824eadefff846e0101e3fc1e
--- /dev/null
+++ b/crates/canvas/assets/tags/paperclip.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/canvas/assets/tags/people.svg b/crates/canvas/assets/tags/people.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9e05bbba8504a877d66ce3f1abde285ec779ef81
--- /dev/null
+++ b/crates/canvas/assets/tags/people.svg
@@ -0,0 +1,12 @@
+
diff --git a/crates/canvas/assets/tags/person.svg b/crates/canvas/assets/tags/person.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c96629370d822862db07e8d2527d6299d38a305b
--- /dev/null
+++ b/crates/canvas/assets/tags/person.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/phone-clock.svg b/crates/canvas/assets/tags/phone-clock.svg
new file mode 100644
index 0000000000000000000000000000000000000000..509dbb63c83db7f1fc8a4a7c646470959ae97387
--- /dev/null
+++ b/crates/canvas/assets/tags/phone-clock.svg
@@ -0,0 +1,26 @@
+
diff --git a/crates/canvas/assets/tags/pink-square.svg b/crates/canvas/assets/tags/pink-square.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a31efcd9104e243fef08a9695d78bb2c081c47fe
--- /dev/null
+++ b/crates/canvas/assets/tags/pink-square.svg
@@ -0,0 +1,19 @@
+
diff --git a/crates/canvas/assets/tags/plane.svg b/crates/canvas/assets/tags/plane.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b73cf45d1d3fff97abbf4786cf0ff11d97d620da
--- /dev/null
+++ b/crates/canvas/assets/tags/plane.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/presentation.svg b/crates/canvas/assets/tags/presentation.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3fa77747b6e49935491d701df0f5c069913ae078
--- /dev/null
+++ b/crates/canvas/assets/tags/presentation.svg
@@ -0,0 +1,15 @@
+
diff --git a/crates/canvas/assets/tags/pushpin.svg b/crates/canvas/assets/tags/pushpin.svg
new file mode 100644
index 0000000000000000000000000000000000000000..285123b9ef713c597c843f1adaba032a68be6df3
--- /dev/null
+++ b/crates/canvas/assets/tags/pushpin.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/canvas/assets/tags/question-balloon.svg b/crates/canvas/assets/tags/question-balloon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..afaf82230a868f0569bcedc270787cae7d20abbb
--- /dev/null
+++ b/crates/canvas/assets/tags/question-balloon.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/quote.svg b/crates/canvas/assets/tags/quote.svg
new file mode 100644
index 0000000000000000000000000000000000000000..56fea2e58a6d3ed557ea9b408c7e9f142424ee72
--- /dev/null
+++ b/crates/canvas/assets/tags/quote.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/research.svg b/crates/canvas/assets/tags/research.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c95b06b8973e3ddb50e11c9025d801924bc4c53e
--- /dev/null
+++ b/crates/canvas/assets/tags/research.svg
@@ -0,0 +1,14 @@
+
diff --git a/crates/canvas/assets/tags/ribbon.svg b/crates/canvas/assets/tags/ribbon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c91860ebb44eeb00ca5db6a26d9e393e36b31a9d
--- /dev/null
+++ b/crates/canvas/assets/tags/ribbon.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/rose.svg b/crates/canvas/assets/tags/rose.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d263560e8b56c6361a736164afa497512750ca82
--- /dev/null
+++ b/crates/canvas/assets/tags/rose.svg
@@ -0,0 +1,10 @@
+
diff --git a/crates/canvas/assets/tags/schedule.svg b/crates/canvas/assets/tags/schedule.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4f23380360a7af6dc31997c1d6df597ed1de3da8
--- /dev/null
+++ b/crates/canvas/assets/tags/schedule.svg
@@ -0,0 +1,30 @@
+
diff --git a/crates/canvas/assets/tags/smiley.svg b/crates/canvas/assets/tags/smiley.svg
new file mode 100644
index 0000000000000000000000000000000000000000..db13981ee4266c3f8b0ba4513e02985cb8786005
--- /dev/null
+++ b/crates/canvas/assets/tags/smiley.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/solid-target-blue.svg b/crates/canvas/assets/tags/solid-target-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..483b645f5d773aa58fbf537f8d1ac58a974cc93c
--- /dev/null
+++ b/crates/canvas/assets/tags/solid-target-blue.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/solid-target-green.svg b/crates/canvas/assets/tags/solid-target-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4ab536a38bbf22a5016d62d6c1b3408d61aadaaa
--- /dev/null
+++ b/crates/canvas/assets/tags/solid-target-green.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/solid-target-yellow.svg b/crates/canvas/assets/tags/solid-target-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..84534fa350815c8911fdb133ab190c228f81441c
--- /dev/null
+++ b/crates/canvas/assets/tags/solid-target-yellow.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/star-blue.svg b/crates/canvas/assets/tags/star-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..513607f50d639f609ec719cbc3e0dc8233e9bc53
--- /dev/null
+++ b/crates/canvas/assets/tags/star-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/star-green.svg b/crates/canvas/assets/tags/star-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4c9e878d5745479b09d166449125509a9926069d
--- /dev/null
+++ b/crates/canvas/assets/tags/star-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/star8-blue.svg b/crates/canvas/assets/tags/star8-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fda1bd0f93ec7aef62ebfc48b21e237985a873c9
--- /dev/null
+++ b/crates/canvas/assets/tags/star8-blue.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/canvas/assets/tags/star8-yellow.svg b/crates/canvas/assets/tags/star8-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c8c440467616655d5a0b5ac0cbc2c7b4587107cb
--- /dev/null
+++ b/crates/canvas/assets/tags/star8-yellow.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/canvas/assets/tags/sun-blue.svg b/crates/canvas/assets/tags/sun-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0c3e2e94d26241019449993c848617ee42577d3a
--- /dev/null
+++ b/crates/canvas/assets/tags/sun-blue.svg
@@ -0,0 +1,16 @@
+
diff --git a/crates/canvas/assets/tags/sun-green.svg b/crates/canvas/assets/tags/sun-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fb3e37f0564e52e7d2884cd4fbb7445f5f29a06d
--- /dev/null
+++ b/crates/canvas/assets/tags/sun-green.svg
@@ -0,0 +1,16 @@
+
diff --git a/crates/canvas/assets/tags/sun-yellow.svg b/crates/canvas/assets/tags/sun-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b0975d0c2f9265bb03dd940c99a2247c836d1d52
--- /dev/null
+++ b/crates/canvas/assets/tags/sun-yellow.svg
@@ -0,0 +1,16 @@
+
diff --git a/crates/canvas/assets/tags/sunflower.svg b/crates/canvas/assets/tags/sunflower.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1ec8e06903b1af3b555e390c5b8a1db89d314f4e
--- /dev/null
+++ b/crates/canvas/assets/tags/sunflower.svg
@@ -0,0 +1,31 @@
+
diff --git a/crates/canvas/assets/tags/target-blue.svg b/crates/canvas/assets/tags/target-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2dc83f4fe091a2488df5758040fd37ea3aba26fa
--- /dev/null
+++ b/crates/canvas/assets/tags/target-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/target-green.svg b/crates/canvas/assets/tags/target-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..965dac80be74e5d50ecccdb55a1ed1d8a6383953
--- /dev/null
+++ b/crates/canvas/assets/tags/target-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/target-yellow.svg b/crates/canvas/assets/tags/target-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..52450cfb864b904145230d3338f3ec3f0745d764
--- /dev/null
+++ b/crates/canvas/assets/tags/target-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/tick-blue.svg b/crates/canvas/assets/tags/tick-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..01763e7be7b4b0fcd05643d1d669c5e1ed8730ba
--- /dev/null
+++ b/crates/canvas/assets/tags/tick-blue.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/tick-green.svg b/crates/canvas/assets/tags/tick-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..830da86e4e985132f6221b907154abe7f7cc1d7a
--- /dev/null
+++ b/crates/canvas/assets/tags/tick-green.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/tick-yellow.svg b/crates/canvas/assets/tags/tick-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a9b64efdb4c4c365940e90dc6565a566a7d26c22
--- /dev/null
+++ b/crates/canvas/assets/tags/tick-yellow.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/triangle-blue.svg b/crates/canvas/assets/tags/triangle-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4f3ca3f1e8e290eb62e105a2f66edd3e73d6446f
--- /dev/null
+++ b/crates/canvas/assets/tags/triangle-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/triangle-green.svg b/crates/canvas/assets/tags/triangle-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9b123bcd0223c15b4f4905d7aa41a4e2d4ac50d4
--- /dev/null
+++ b/crates/canvas/assets/tags/triangle-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/triangle-yellow.svg b/crates/canvas/assets/tags/triangle-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5ef6040830c8931b762a1d4558a1acdd172b1064
--- /dev/null
+++ b/crates/canvas/assets/tags/triangle-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/umbrella-blue.svg b/crates/canvas/assets/tags/umbrella-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f7ff89e7a54ed5ff03b89353109cfda2017b3912
--- /dev/null
+++ b/crates/canvas/assets/tags/umbrella-blue.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/umbrella-green.svg b/crates/canvas/assets/tags/umbrella-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f366e4f8662cc84442a7ae2a80181581229ee945
--- /dev/null
+++ b/crates/canvas/assets/tags/umbrella-green.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/umbrella-yellow.svg b/crates/canvas/assets/tags/umbrella-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9af9dab87f409b9391729eb0a4e4fda0316cbb34
--- /dev/null
+++ b/crates/canvas/assets/tags/umbrella-yellow.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/x-blue.svg b/crates/canvas/assets/tags/x-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6a9a21b9ecef7dc8c9b78776024ca88f6a67e1ef
--- /dev/null
+++ b/crates/canvas/assets/tags/x-blue.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/canvas/assets/tags/x-dots-blue.svg b/crates/canvas/assets/tags/x-dots-blue.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2c07fb31412e963d69f966ac5fab32bb0605f8a7
--- /dev/null
+++ b/crates/canvas/assets/tags/x-dots-blue.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/x-dots-green.svg b/crates/canvas/assets/tags/x-dots-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f8218cbe74fd363fc907cba32eb6aad2ebcef64c
--- /dev/null
+++ b/crates/canvas/assets/tags/x-dots-green.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/x-dots-yellow.svg b/crates/canvas/assets/tags/x-dots-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..152672099416b4b9eef079a1fa416a5d56bfe563
--- /dev/null
+++ b/crates/canvas/assets/tags/x-dots-yellow.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/x-green.svg b/crates/canvas/assets/tags/x-green.svg
new file mode 100644
index 0000000000000000000000000000000000000000..596ebd0ab372806bb08206cf43028f0b2e39c9ff
--- /dev/null
+++ b/crates/canvas/assets/tags/x-green.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/canvas/assets/tags/x-yellow.svg b/crates/canvas/assets/tags/x-yellow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9477943df1501075574c17aa39060b06346efa6b
--- /dev/null
+++ b/crates/canvas/assets/tags/x-yellow.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/canvas/src/bin/page_probe.rs b/crates/canvas/src/bin/page_probe.rs
index f3fcad3abe3417881eb3de561e44957037526a05..fe226164b10660e47ea37c2d0f52eb5dde704362 100644
--- a/crates/canvas/src/bin/page_probe.rs
+++ b/crates/canvas/src/bin/page_probe.rs
@@ -65,6 +65,9 @@ fn main() -> Result<(), Box> {
PageObject::Image(image) => objects.push(json!({"kind": "image", "id": image.id,
"layout": image.layout, "bytes": image.bytes.as_ref().map(|b| b.len()),
"alt": image.alt, "background": image.background})),
+ PageObject::Attachment(file) => objects.push(json!({"kind": "file", "id": file.id,
+ "layout": file.layout, "filename": file.filename,
+ "bytes": file.bytes.as_ref().map(|b| b.len())})),
PageObject::Ink(ink) => objects.push(json!({"kind": "ink", "id": ink.id,
"bounds": ink.bounds(), "strokes": ink.strokes.len(), "groups": ink.groups.len()})),
PageObject::Unsupported(object) => objects.push(json!({"kind": "unsupported",
diff --git a/crates/canvas/src/editor.rs b/crates/canvas/src/editor.rs
index eb296741af44f2ac8598b4089186579b11ed943d..33f408bb3aaa50fc812d364d98ca855e0c238248 100644
--- a/crates/canvas/src/editor.rs
+++ b/crates/canvas/src/editor.rs
@@ -117,6 +117,9 @@ pub struct CanvasEditor {
/// What Select All widened to, holding until the selection is moved or set, and while the
/// focused outline and its selection stay as they were then.
whole: Option<(Whole, ExGuid, Selection)>,
+ /// The outline, caret and undo depth a run of typing left, which typing there joins as
+ /// OneNote 2010 does until the caret moves or another kind of edit comes between.
+ typing: Option<(ExGuid, TextPosition, usize)>,
}
/// Imported page state the editable content does not carry.
@@ -804,6 +807,12 @@ enum History {
index: usize,
image: Option>,
},
+ /// Restores the file on the page at `index` in paint order, or removes the one there
+ /// when `None`.
+ File {
+ index: usize,
+ file: Option>,
+ },
Remove {
outline: onestore::ExGuid,
focus: RestoreFocus,
@@ -860,6 +869,7 @@ impl CanvasEditor {
ops: Ok(Vec::new()),
stored: None,
whole: None,
+ typing: None,
})
}
@@ -877,6 +887,7 @@ impl CanvasEditor {
.iter()
.filter_map(|object| match object {
page::Content::ReadOnly(object) => Some(object.rect()[2]),
+ page::Content::File { .. } => object.file().map(|(_, rect)| rect[2]),
page::Content::Ink(ink) => page::ink_bounds(ink).map(|b| b[2]),
_ => None,
})
@@ -1047,7 +1058,9 @@ impl CanvasEditor {
let reaches_one = |history: &History| match history {
History::Date(_) => date_changed,
History::Paper { .. } => false,
- History::Image { .. } | History::Picture { .. } => objects_changed,
+ History::Image { .. } | History::Picture { .. } | History::File { .. } => {
+ objects_changed
+ }
History::Draft { outlines, .. } => changed.contains(&outlines[0].id),
History::Text { outline, change } => {
changed.contains(outline)
@@ -1070,10 +1083,15 @@ impl CanvasEditor {
History::Group { entries, .. } => entries.iter().any(reaches_one),
history => reaches_one(history),
};
+ // A typing run goes on while the change leaves its undo step alone.
+ let run = self
+ .typing
+ .filter(|_| self.undo.last().is_some_and(|last| !reaches(last)));
fresh.undo = std::mem::take(&mut self.undo)
.into_iter()
.filter(|history| !reaches(history))
.collect();
+ fresh.typing = run.map(|(outline, caret, _)| (outline, caret, fresh.undo.len()));
fresh.redo = std::mem::take(&mut self.redo)
.into_iter()
.filter(|history| !reaches(history))
@@ -1108,6 +1126,10 @@ impl CanvasEditor {
objects.push(PageObject::Image(image.clone()));
continue;
}
+ page::Content::File { source, .. } => {
+ objects.push(PageObject::Attachment(source.clone()));
+ continue;
+ }
page::Content::Ink(ink) => {
objects.push(PageObject::Ink(ink.clone()));
continue;
@@ -1423,6 +1445,7 @@ impl CanvasEditor {
ops: Ok(Vec::new()),
stored: None,
whole: None,
+ typing: None,
})
}
@@ -1455,23 +1478,99 @@ impl CanvasEditor {
Ok(true)
}
- /// Gives the page `color` (View, Page Color) and `rule_lines` (View, Rule Lines) as one
- /// undo step; false when it had them.
+ /// Gives the page `color` (View, Page Color), `rule_lines` (View, Rule Lines) and, when
+ /// given, `art` in place of its background pictures, as one undo step; false when it had
+ /// them.
pub fn set_paper(
&mut self,
color: Option,
rule_lines: Option,
+ art: Option>,
) -> bool {
- if (color, rule_lines) == (self.header.color, self.header.rule_lines) {
+ let backgrounds: Vec = (0..self.objects.len())
+ .filter(|&index| {
+ self.objects[index]
+ .picture()
+ .is_some_and(|image| image.background)
+ })
+ .collect();
+ let art = art.filter(|art| {
+ art.len() != backgrounds.len()
+ || art.iter().zip(&backgrounds).any(|(new, &index)| {
+ let shown = self.objects[index].picture().unwrap();
+ (&new.layout, new.size, &new.bytes) != (&shown.layout, shown.size, &shown.bytes)
+ })
+ });
+ let paper = (color, rule_lines) != (self.header.color, self.header.rule_lines);
+ if !paper && art.is_none() {
return false;
}
self.finish_composition();
- let before = self.paper(color, rule_lines);
- self.undo.push(before);
+ let mut entries = Vec::new();
+ if paper {
+ entries.push(self.paper(color, rule_lines));
+ }
+ if let Some(art) = art {
+ for index in backgrounds.into_iter().rev() {
+ let image = self.remove_picture(index);
+ entries.push(History::Picture {
+ index,
+ image: Some(Box::new(image)),
+ });
+ }
+ // Art lies under everything else on the page.
+ for image in art.into_iter().rev() {
+ self.add_picture(0, image);
+ entries.push(History::Picture {
+ index: 0,
+ image: None,
+ });
+ }
+ }
+ self.undo.push(match entries.len() {
+ 1 => entries.pop().unwrap(),
+ _ => History::Group {
+ entries,
+ page: false,
+ },
+ });
self.redo.clear();
true
}
+ /// Puts page-level picture `image` at `index` in paint order, recording its op.
+ fn add_picture(&mut self, index: usize, image: onestore::page::Image) {
+ let object = PageObject::Image(image.clone());
+ self.objects.insert(index, page::Content::Image(image));
+ let before = self.successor(object.id());
+ self.record(Ok(vec![PageOp::Add { object, before }]));
+ }
+
+ /// Takes the page-level picture at `index` in paint order off the page, recording its op.
+ fn remove_picture(&mut self, index: usize) -> onestore::page::Image {
+ let image = self.objects[index].picture().unwrap().clone();
+ self.objects.remove(index);
+ self.record(Ok(vec![PageOp::Delete { object: image.id }]));
+ image
+ }
+
+ /// Puts a file on the page, `file` being its `Content::File`, at `index` in paint order,
+ /// recording its op.
+ fn add_file(&mut self, index: usize, file: page::Content) {
+ let object = PageObject::Attachment(file.file().unwrap().0.clone());
+ self.objects.insert(index, file);
+ let before = self.successor(object.id());
+ self.record(Ok(vec![PageOp::Add { object, before }]));
+ }
+
+ /// Takes the file at `index` in paint order off the page, recording its op.
+ fn remove_file(&mut self, index: usize) -> page::Content {
+ let file = self.objects.remove(index);
+ let object = file.file().unwrap().0.id;
+ self.record(Ok(vec![PageOp::Delete { object }]));
+ file
+ }
+
/// Shows `color` and `rule_lines`, recording the ops that store them; the history entry
/// restoring what the page had.
fn paper(
@@ -1754,9 +1853,12 @@ impl CanvasEditor {
Ok(())
}
- /// Origin and size of a picture the user can select; OneNote passes clicks through
- /// backgrounds.
+ /// Origin and size of a picture or file on the page the user can select; OneNote passes
+ /// clicks through backgrounds.
pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> {
+ if let Some((_, [x0, y0, x1, y1])) = self.page_file(id) {
+ return Some(([x0, y0], [x1 - x0, y1 - y0]));
+ }
if let Some(image) = self.image(id) {
let layout = &image.layout;
return Some((
@@ -1776,20 +1878,50 @@ impl CanvasEditor {
self.outline_picture(id).is_some()
}
- /// A picture held as a paragraph: its outline, the paragraph's container and index there,
- /// and the paragraph.
+ /// A picture or file held as a paragraph: its outline, the paragraph's container and index
+ /// there, and the paragraph.
fn outline_picture(
&self,
id: ExGuid,
) -> Option<(ExGuid, Option, usize, &PageParagraph)> {
self.outlines.iter().find_map(|outline| {
descendants(outline.document.nodes(), None).find_map(|(container, index, node)| {
- matches!(&node.content, onestore::page::ParagraphContent::Image(image) if image.id == id)
- .then_some((outline.id, container, index, node))
+ holds(&node.content, id).then_some((outline.id, container, index, node))
})
})
}
+ /// The file a paragraph holds or the page shows, which the host opens and saves.
+ pub fn attachment(&self, id: ExGuid) -> Option<&onestore::page::Attachment> {
+ if let Some((file, _)) = self.page_file(id) {
+ return Some(file);
+ }
+ match &self.outline_picture(id)?.3.content {
+ ParagraphContent::Attachment(file) => Some(file),
+ _ => None,
+ }
+ }
+
+ /// A file on the page and where its column lies, in page coordinates.
+ fn page_file(&self, id: ExGuid) -> Option<(&onestore::page::Attachment, [f32; 4])> {
+ self.objects
+ .iter()
+ .filter_map(page::Content::file)
+ .find(|(file, _)| file.id == id)
+ }
+
+ /// Where a file draws with its name, in page coordinates.
+ pub fn attachment_rect(&self, id: ExGuid) -> Option<[f32; 4]> {
+ if let Some((_, rect)) = self.page_file(id) {
+ return Some(rect);
+ }
+ let (outline, ..) = self.outline_picture(id)?;
+ let outline = self.outlines.iter().find(|item| item.id == outline)?;
+ let [x0, y0, x1, y1] = outline.shaped.objects.iter().find(|o| o.id == id)?.bounds();
+ let [x, y] = outline.origin();
+ Some([x + x0, y + y0, x + x1, y + y1])
+ }
+
/// Replaces a picture paragraph through the outline's text history.
fn edit_outline_picture(
&mut self,
@@ -1827,8 +1959,8 @@ impl CanvasEditor {
})
}
- /// Moves a picture, and resizes it when `size` differs from the stored size. A picture in
- /// an outline's flow keeps its place.
+ /// Moves a picture or a file on the page, and resizes a picture when `size` differs from
+ /// the stored size. A picture in an outline's flow keeps its place.
pub fn place_image(
&mut self,
engine: &mut TextEngine,
@@ -1844,7 +1976,7 @@ impl CanvasEditor {
if let Some((.., node)) = self.outline_picture(id) {
let mut node = node.clone();
let onestore::page::ParagraphContent::Image(image) = &mut node.content else {
- unreachable!()
+ return Ok(());
};
if crate::outline::image_size(image) == Some(size) {
return Ok(());
@@ -1853,6 +1985,25 @@ impl CanvasEditor {
image.layout.width_set_by_user = Some(true);
return self.edit_outline_picture(engine, id, Some(node));
}
+ if let Some((file, _)) = self.page_file(id) {
+ let previous = [file.layout.x, file.layout.y];
+ if previous == origin.map(Some) {
+ return Ok(());
+ }
+ self.finish_composition();
+ let layout = self.object_layout_mut(id).unwrap();
+ [layout.x, layout.y] = origin.map(Some);
+ self.undo.push(History::Position {
+ object: id,
+ position: previous,
+ });
+ self.redo.clear();
+ self.record(self.placement_ops(&Placement {
+ id,
+ position: previous,
+ }));
+ return Ok(());
+ }
let image = self.image(id).ok_or(EditError::InvalidRange)?;
let mut layout = image.layout.clone();
[layout.x, layout.y] = origin.map(Some);
@@ -1897,9 +2048,7 @@ impl CanvasEditor {
let mut passed = false;
for (_, _, node) in descendants(source.document.nodes(), None) {
match &node.content {
- onestore::page::ParagraphContent::Image(image) if image.id == id => {
- passed = true
- }
+ content if holds(content, id) => passed = true,
onestore::page::ParagraphContent::Text(text) => {
let end = text.text.utf16_offset(text.text.text().len())?;
if passed {
@@ -1925,7 +2074,10 @@ impl CanvasEditor {
let index = self
.objects
.iter()
- .position(|object| matches!(object, page::Content::Image(image) if image.id == id))
+ .position(|object| {
+ matches!(object, page::Content::Image(image) if image.id == id)
+ || object.file().is_some_and(|(file, _)| file.id == id)
+ })
.ok_or(EditError::InvalidRange)?;
// An emptied outline keeps its slot for undo after leaving `outlines`.
let editable = |object: &page::Content| match object {
@@ -1989,10 +2141,95 @@ impl CanvasEditor {
)
}
+ /// Attaches `file` at the caret as OneNote 2010 does (`corpus/attachment-insert`): the
+ /// caret's paragraph splits there, the file takes the place of an empty first half, and the
+ /// caret starts the second. A caret on blank page puts the file on the page there
+ /// (`corpus/attachment-floating`).
+ pub fn insert_attachment(
+ &mut self,
+ engine: &mut TextEngine,
+ mut file: onestore::page::Attachment,
+ ) -> Result<(), EditorError> {
+ if let Focus::Caret { outline, .. } = &self.active {
+ [file.layout.x, file.layout.y] = outline.origin().map(Some);
+ let layout = Box::new(crate::outline::page_file(engine, &file)?);
+ let [x0, y0, x1, y1] = layout.bounds();
+ [file.layout.max_width, file.layout.max_height] = [Some(x1 - x0), Some(y1 - y0)];
+ file.layout.width_set_by_user = Some(false);
+ self.finish_composition();
+ let index = self.objects.len();
+ self.add_file(
+ index,
+ page::Content::File {
+ source: file,
+ layout,
+ },
+ );
+ self.undo.push(History::File { index, file: None });
+ self.redo.clear();
+ return Ok(());
+ }
+ let [anchor, focus] = self.selection().positions;
+ let (start, end) = (anchor.min(focus), anchor.max(focus));
+ let mut format = self.typing_format(start)?;
+ link::unlinked(&mut format);
+ let mut edit = self
+ .active_outline()
+ .document
+ .replace(start..end, vec![Paragraph::new(String::new(), format); 2])?;
+ let [head, tail, ..] = &mut edit.replacement[..] else {
+ unreachable!("a split holds both halves")
+ };
+ let node = PageParagraph {
+ id: onestore::page::text::new_id()?,
+ content: ParagraphContent::Attachment(file),
+ style: None,
+ lists: Vec::new(),
+ tags: Vec::new(),
+ collapsed: false,
+ ..head.clone()
+ };
+ let empty = head.text().unwrap().text.text().is_empty();
+ if empty {
+ tail.tags = std::mem::take(&mut head.tags);
+ tail.text_mut().unwrap().tags = std::mem::take(&mut head.text_mut().unwrap().tags);
+ }
+ edit.replacement.insert(1, node);
+ if empty {
+ edit.replacement.remove(0);
+ }
+ let caret = TextPosition {
+ paragraph: start.paragraph + usize::from(!empty),
+ offset: 0,
+ };
+ self.commit(
+ engine,
+ edit,
+ Selection {
+ positions: [caret; 2],
+ affinities: [Affinity::Upstream; 2],
+ },
+ )
+ }
+
pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> {
if self.outline_picture(id).is_some() {
return self.edit_outline_picture(engine, id, None);
}
+ if let Some(index) = self
+ .objects
+ .iter()
+ .position(|object| object.file().is_some_and(|(file, _)| file.id == id))
+ {
+ self.finish_composition();
+ let file = self.remove_file(index);
+ self.undo.push(History::File {
+ index,
+ file: Some(Box::new(file)),
+ });
+ self.redo.clear();
+ return Ok(());
+ }
let index = self
.objects
.iter()
@@ -2001,15 +2238,12 @@ impl CanvasEditor {
})
.ok_or(EditError::InvalidRange)?;
self.finish_composition();
- let page::Content::Image(image) = self.objects.remove(index) else {
- unreachable!()
- };
+ let image = self.remove_picture(index);
self.undo.push(History::Picture {
index,
image: Some(Box::new(image)),
});
self.redo.clear();
- self.record(Ok(vec![PageOp::Delete { object: id }]));
Ok(())
}
@@ -2227,6 +2461,55 @@ impl CanvasEditor {
result
}
+ /// Runs `edit`, typing or a backspace at a caret, as part of the typing run the last undo
+ /// step holds if the caret is where that run left it and nothing came between; OneNote 2010
+ /// undoes a run whole however long it pauses.
+ fn typed(
+ &mut self,
+ edit: impl FnOnce(&mut Self) -> Result,
+ ) -> Result {
+ let [anchor, focus] = self.selection().positions;
+ let caret = match self.marked_range() {
+ Some(marked) => Some(marked.start),
+ None => (anchor == focus).then_some(focus),
+ };
+ let joins = self.typing.take().is_some_and(|(outline, at, depth)| {
+ outline == self.active_outline().id
+ && caret == Some(at)
+ && depth == self.undo.len()
+ && self.pending.is_none()
+ });
+ let depth = self.undo.len() - usize::from(joins);
+ let result = edit(self);
+ let (mut run, mut page) = (Vec::new(), false);
+ for entry in self.undo.split_off(depth) {
+ match entry {
+ History::Group {
+ entries,
+ page: selected,
+ } => {
+ page |= selected;
+ if run.is_empty() {
+ run = entries;
+ } else {
+ run.extend(entries);
+ }
+ }
+ entry => run.push(entry),
+ }
+ }
+ match run.len() {
+ 0 => return result,
+ 1 if !page => self.undo.extend(run),
+ _ => self.undo.push(History::Group { entries: run, page }),
+ }
+ if result.is_ok() {
+ let caret = self.selection().positions[1];
+ self.typing = Some((self.active_outline().id, caret, self.undo.len()));
+ }
+ result
+ }
+
/// Removes the page selection's outlines, last first, but its last when `keep`, whose
/// text stays selected to be replaced. Deleting the page selection leaves a caret where
/// its first outline began, as OneNote 2010's does.
@@ -2375,6 +2658,7 @@ impl CanvasEditor {
) -> Result<(), EditorError> {
self.finish_composition();
self.whole = None;
+ self.typing = None;
if !extend && movement == Movement::Right && self.leave_equation()? {
return Ok(());
}
@@ -2842,12 +3126,21 @@ impl CanvasEditor {
)
}
- /// Types `text` at the selection; a space ends a typed URL, which becomes a link.
+ /// Types `text` at the selection; a space ends a typed URL, which becomes a link. A line
+ /// of text joins the typing run before it.
pub fn insert(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
+ if text.is_empty() || text.contains('\n') {
+ self.insert_text(engine, text)
+ } else {
+ self.typed(|editor| editor.insert_text(engine, text))
+ }
+ }
+
+ fn insert_text(&mut self, engine: &mut TextEngine, text: &str) -> Result<(), EditorError> {
if self.page_selected() {
return self.grouped(|editor| {
editor.remove_page(engine, true)?;
- editor.insert(engine, text)
+ editor.insert_text(engine, text)
});
}
self.leave_link_code()?;
@@ -3137,7 +3430,8 @@ impl CanvasEditor {
let selection = self.active_outline().selection;
let [anchor, focus] = selection.positions;
let mut range = anchor.min(focus)..anchor.max(focus);
- if range.is_empty() {
+ let at_caret = range.is_empty();
+ if at_caret {
let outline = self.active_outline();
let paragraph = outline.paragraph_layout(focus.paragraph)?;
let cursor = paragraph.cursor(focus.offset, selection.affinities[1])?;
@@ -3213,7 +3507,11 @@ impl CanvasEditor {
onestore::page::text::Affinity::Upstream,
)?;
}
- self.delete_range(engine, range)
+ if backward && at_caret {
+ self.typed(|editor| editor.delete_range(engine, range))
+ } else {
+ self.delete_range(engine, range)
+ }
}
pub fn delete_to(
@@ -3226,19 +3524,26 @@ impl CanvasEditor {
}
self.finish_composition();
let original = self.selection();
- let preferred_x = self.preferred_x;
- if original.positions[0] == original.positions[1] {
+ let (preferred_x, typing) = (self.preferred_x, self.typing);
+ let [caret, focus] = original.positions;
+ let collapsed = caret == focus;
+ if collapsed {
let result = self.move_selection(engine, movement, true);
if let Err(error) = result {
self.active_outline_mut().selection = original;
- self.preferred_x = preferred_x;
+ (self.preferred_x, self.typing) = (preferred_x, typing);
return Err(error);
}
}
let [anchor, focus] = self.selection().positions;
self.active_outline_mut().selection = original;
- self.preferred_x = preferred_x;
- self.delete_range(engine, anchor.min(focus)..anchor.max(focus))
+ (self.preferred_x, self.typing) = (preferred_x, typing);
+ let range = anchor.min(focus)..anchor.max(focus);
+ if collapsed && range.end == caret && range.start.paragraph == caret.paragraph {
+ self.typed(|editor| editor.delete_range(engine, range))
+ } else {
+ self.delete_range(engine, range)
+ }
}
fn delete_range(
@@ -3326,6 +3631,7 @@ impl CanvasEditor {
history: History,
) -> Result {
self.pending = None;
+ self.typing = None;
let valid = match &history {
History::Date(date) => self
.date
@@ -3363,9 +3669,20 @@ impl CanvasEditor {
index,
image: Some(_),
} => *index <= self.objects.len(),
- History::Picture { index, image: None } => {
- matches!(self.objects.get(*index), Some(page::Content::Image(_)))
- }
+ History::Picture { index, image: None } => self
+ .objects
+ .get(*index)
+ .and_then(page::Content::picture)
+ .is_some(),
+ History::File {
+ index,
+ file: Some(_),
+ } => *index <= self.objects.len(),
+ History::File { index, file: None } => self
+ .objects
+ .get(*index)
+ .and_then(page::Content::file)
+ .is_some(),
History::Remove { outline, focus } => {
self.outlines.iter().any(|item| item.id == *outline)
&& match focus {
@@ -3584,24 +3901,20 @@ impl CanvasEditor {
index,
image: match image {
Some(image) => {
- self.objects.insert(index, page::Content::Image(*image));
- let page::Content::Image(image) = &self.objects[index] else {
- unreachable!()
- };
- let ops = vec![PageOp::Add {
- object: PageObject::Image(image.clone()),
- before: self.successor(image.id),
- }];
- self.record(Ok(ops));
+ self.add_picture(index, *image);
None
}
- None => match self.objects.remove(index) {
- page::Content::Image(image) => {
- self.record(Ok(vec![PageOp::Delete { object: image.id }]));
- Some(Box::new(image))
- }
- _ => unreachable!(),
- },
+ None => Some(Box::new(self.remove_picture(index))),
+ },
+ },
+ History::File { index, file } => History::File {
+ index,
+ file: match file {
+ Some(file) => {
+ self.add_file(index, *file);
+ None
+ }
+ None => Some(Box::new(self.remove_file(index))),
},
},
History::Remove { outline, focus } => {
@@ -3827,9 +4140,17 @@ impl CanvasEditor {
.count()
.try_into()
.map_err(|_| EditError::TextTooLong)?;
- self.compose(engine, text, end..end)?;
- self.finish_composition();
- Ok(())
+ let lines = text.contains('\n');
+ let commit = |editor: &mut Self| {
+ editor.compose(engine, text, end..end)?;
+ editor.finish_composition();
+ Ok(())
+ };
+ if lines {
+ commit(self)
+ } else {
+ self.typed(commit)
+ }
} else {
self.insert(engine, &text)
}
@@ -4128,6 +4449,15 @@ impl CanvasEditor {
/// Where `position` in `old` lies in `new`, the same outline changed elsewhere: in the same
/// paragraph, past what changed in its text when it lies after it; at the start of the
/// paragraph now at its place when that one is gone.
+/// Whether `content` is picture or file `id`.
+fn holds(content: &ParagraphContent, id: ExGuid) -> bool {
+ match content {
+ ParagraphContent::Image(image) => image.id == id,
+ ParagraphContent::Attachment(file) => file.id == id,
+ _ => false,
+ }
+}
+
fn follow(old: &TextDocument, new: &TextDocument, position: TextPosition) -> TextPosition {
let found = old.leaf(position.paragraph).and_then(|(_, _, node)| {
let paragraph = new
@@ -5065,22 +5395,74 @@ mod tests {
editor.active_outline().shaped.paragraphs.len(),
fresh.paragraphs.len()
);
- editor.undo(&mut engine).unwrap();
- assert_eq!(
- editor
- .active_outline()
- .document
- .paragraphs()
- .last()
- .unwrap()
- .text(),
- "Lasta"
- );
+ // The committed composition and the typing after it are one run.
editor.undo(&mut engine).unwrap();
assert_eq!(editor.outlines()[0].document, source);
assert_eq!(editor.active_outline().document, draft);
}
+ /// OneNote 2010's undo steps (Windows 7 lab, 2026-09-29): typing, backspaces and word
+ /// backspaces within a paragraph are one step however long the pauses; a caret move, Enter,
+ /// a backspace joining paragraphs, Delete, paste and a format chosen at the caret each end it.
+ #[test]
+ fn typing_runs_undo_whole_until_the_caret_moves_or_another_edit() {
+ let mut engine = TextEngine::default();
+ let source =
+ TextDocument::new(vec![Paragraph::new("Base".into(), Format::default())]).unwrap();
+ let mut editor = CanvasEditor::new(&mut engine, source, 240.0).unwrap();
+ let at = |paragraph, offset| [TextPosition { paragraph, offset }; 2].into();
+ let text = |editor: &CanvasEditor| editor.active_outline().shown_text();
+ editor.select(at(0, 4)).unwrap();
+ editor.insert(&mut engine, " quick").unwrap();
+ editor.insert(&mut engine, " brown").unwrap();
+ editor.delete(&mut engine, true).unwrap();
+ editor.delete_to(&mut engine, Movement::WordLeft).unwrap();
+ editor.commit_text(&mut engine, "fox".into()).unwrap();
+ editor.select(editor.selection()).unwrap();
+ editor.insert(&mut engine, " jumps").unwrap();
+ assert_eq!(text(&editor), "Base quick fox jumps");
+ assert!(editor.undo(&mut engine).unwrap());
+ assert_eq!(text(&editor), "Base");
+ assert_eq!(editor.selection().positions, [at(0, 4).positions[0]; 2]);
+ assert!(!editor.can_undo());
+ assert!(editor.redo(&mut engine).unwrap());
+ assert_eq!(text(&editor), "Base quick fox jumps");
+
+ let mut shown = vec!["Base".to_owned(), text(&editor)];
+ for movement in [Movement::Left, Movement::Right] {
+ editor.move_selection(&mut engine, movement, false).unwrap();
+ }
+ editor.insert(&mut engine, "!").unwrap();
+ editor.insert(&mut engine, "?").unwrap();
+ shown.push(text(&editor));
+ editor.enter(&mut engine, false).unwrap();
+ shown.push(text(&editor));
+ editor.insert(&mut engine, "ab").unwrap();
+ shown.push(text(&editor));
+ editor
+ .move_selection(&mut engine, Movement::Left, false)
+ .unwrap();
+ editor.delete(&mut engine, false).unwrap();
+ shown.push(text(&editor));
+ editor.insert(&mut engine, "c").unwrap();
+ shown.push(text(&editor));
+ editor
+ .format(&mut engine, Formatting::Toggle(Toggle::Bold))
+ .unwrap();
+ editor.insert(&mut engine, "d").unwrap();
+ editor.insert(&mut engine, "e").unwrap();
+ shown.push(text(&editor));
+ editor.select(at(1, 0)).unwrap();
+ editor.delete(&mut engine, true).unwrap();
+ shown.push(text(&editor));
+ editor.insert(&mut engine, "f").unwrap();
+ assert_eq!(text(&editor), "Base quick fox jumps!?facde");
+ while editor.undo(&mut engine).unwrap() {
+ assert_eq!(text(&editor), shown.pop().unwrap());
+ }
+ assert!(shown.is_empty());
+ }
+
#[test]
fn table_horizontal_arrows_follow_reading_order() {
let mut engine = TextEngine::default();
@@ -5659,6 +6041,80 @@ mod tests {
assert_eq!(ids(&editor), [background_id]);
}
+ #[test]
+ fn art_and_paper_change_together_as_one_step_under_the_page() {
+ use onestore::page::{Image, Page, PageObject};
+ let mut engine = TextEngine::default();
+ let image = |background, bytes: &[u8]| Image {
+ size: None,
+ id: onestore::page::text::new_id().unwrap(),
+ layout: onestore::document::Layout {
+ x: Some(-27.0),
+ y: Some(-3.6),
+ max_width: Some(174.5),
+ max_height: Some(640.0),
+ ..Default::default()
+ },
+ bytes: Some(std::sync::Arc::from(bytes)),
+ alt: None,
+ background,
+ };
+ let [old, picture] = [image(true, b"old art"), image(false, b"picture")];
+ let mut editor = CanvasEditor::from_page(
+ Page {
+ title: String::new(),
+ identity: None,
+ created: None,
+ margin_origin: [36.0, 14.4],
+ color: None,
+ rule_lines: None,
+ definitions: BTreeMap::new(),
+ objects: vec![
+ PageObject::Image(old.clone()),
+ PageObject::Image(picture.clone()),
+ ],
+ },
+ &mut engine,
+ )
+ .unwrap();
+ let ids = |editor: &CanvasEditor| {
+ let page = editor.page().unwrap();
+ let ids: Vec<_> = page.objects.iter().map(PageObject::id).collect();
+ (ids, page.color)
+ };
+ let art = || [image(true, b"art one"), image(true, b"art two")];
+ let [one, two] = art();
+ let blue = Some(0x00fef5ed);
+ assert!(editor.set_paper(blue, None, Some(vec![one.clone(), two.clone()])));
+ let applied = (vec![one.id, two.id, picture.id], blue);
+ assert_eq!(ids(&editor), applied);
+ assert_eq!(
+ editor.take_ops().unwrap(),
+ [
+ PageOp::Color(blue),
+ PageOp::Delete { object: old.id },
+ PageOp::Add {
+ object: PageObject::Image(two.clone()),
+ before: Some(picture.id),
+ },
+ PageOp::Add {
+ object: PageObject::Image(one.clone()),
+ before: Some(two.id),
+ },
+ ]
+ );
+ // The same art again, under fresh identities, changes nothing.
+ assert!(!editor.set_paper(blue, None, Some(art().into())));
+ assert!(editor.undo(&mut engine).unwrap());
+ assert_eq!(ids(&editor), (vec![old.id, picture.id], None));
+ assert_eq!(editor.take_ops().unwrap().len(), 4);
+ assert!(!editor.can_undo());
+ assert!(editor.redo(&mut engine).unwrap());
+ assert_eq!(ids(&editor), applied);
+ assert!(editor.set_paper(blue, None, Some(Vec::new())));
+ assert_eq!(ids(&editor), (vec![picture.id], blue));
+ }
+
#[test]
fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() {
use onestore::page::{Image, Page, PageObject};
@@ -5858,6 +6314,94 @@ mod tests {
);
}
+ /// OneNote 2010 attaches at the caret (`corpus/attachment-insert`): the text after it
+ /// follows the file and keeps the caret, and an empty first half gives way to the file.
+ #[test]
+ fn a_file_attaches_at_the_caret_as_onenote_splits_around_it() {
+ use onestore::page::{Attachment, Page, PageObject, ParagraphContent};
+ let mut engine = TextEngine::default();
+ let source = TextOutline::new(
+ &mut engine,
+ TextDocument::new(vec![
+ Paragraph::new("Before".into(), Default::default()),
+ Paragraph::new(String::new(), Default::default()),
+ ])
+ .unwrap(),
+ 240.0,
+ [36.0, 36.0],
+ )
+ .unwrap()
+ .snapshot();
+ let mut editor = CanvasEditor::from_page(
+ Page {
+ title: String::new(),
+ identity: None,
+ created: None,
+ margin_origin: [36.0, 14.4],
+ color: None,
+ rule_lines: None,
+ definitions: BTreeMap::new(),
+ objects: vec![PageObject::Outline(source)],
+ },
+ &mut engine,
+ )
+ .unwrap();
+ editor.take_ops().unwrap();
+ let file = |name: &str| Attachment {
+ id: onestore::page::text::new_id().unwrap(),
+ filename: name.into(),
+ source_path: None,
+ size: Some([24.0, 24.0]),
+ layout: Default::default(),
+ bytes: Some(std::sync::Arc::from(b"payload".as_slice())),
+ preview: None,
+ recording: None,
+ };
+ let at = |paragraph, offset| Selection {
+ positions: [TextPosition { paragraph, offset }; 2],
+ affinities: [Affinity::Downstream; 2],
+ };
+ let shown = |editor: &CanvasEditor| {
+ let page = editor.page().unwrap();
+ let PageObject::Outline(outline) = &page.objects[0] else {
+ panic!()
+ };
+ outline
+ .paragraphs
+ .iter()
+ .map(|node| match &node.content {
+ ParagraphContent::Text(text) => text.text.text().to_owned(),
+ ParagraphContent::Attachment(file) => format!("[{}]", file.filename),
+ _ => panic!(),
+ })
+ .collect::>()
+ };
+
+ editor.select(at(0, 3)).unwrap();
+ let middle = file("middle.txt");
+ editor.insert_attachment(&mut engine, middle.clone()).unwrap();
+ assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
+ // A file is no text position: the caret starts "ore", the second text.
+ assert_eq!(editor.selection().positions, at(1, 0).positions);
+ assert_eq!(editor.attachment(middle.id), Some(&middle));
+ let ops = editor.take_ops().unwrap();
+ assert!(ops.iter().any(|op| matches!(op, PageOp::Insert { paragraphs, .. }
+ if paragraphs.iter().any(|p| matches!(&p.content, ParagraphContent::Attachment(a) if *a == middle)))));
+
+ editor.select(at(2, 0)).unwrap();
+ editor.insert_attachment(&mut engine, file("empty.bin")).unwrap();
+ assert_eq!(
+ shown(&editor),
+ ["Bef", "[middle.txt]", "ore", "[empty.bin]", ""]
+ );
+ assert_eq!(editor.selection().positions[0].paragraph, 2);
+
+ assert!(editor.undo(&mut engine).unwrap());
+ assert_eq!(shown(&editor), ["Bef", "[middle.txt]", "ore", ""]);
+ editor.remove_image(&mut engine, middle.id).unwrap();
+ assert_eq!(shown(&editor), ["Bef", "ore", ""]);
+ }
+
#[test]
fn unknown_paragraphs_and_text_free_outlines_stay_on_the_page() {
use onestore::page::{Image, Page, PageObject, ParagraphContent, Unsupported};
@@ -6405,17 +6949,6 @@ mod tests {
assert_eq!(editor.active_outline().shaped.size, expected.shaped.size);
assert_eq!(editor.active_outline().document.nodes().len(), 2);
editor.undo(&mut engine).unwrap();
- assert_eq!(
- editor
- .active_outline()
- .document
- .paragraphs()
- .last()
- .unwrap()
- .text(),
- "a"
- );
- editor.undo(&mut engine).unwrap();
assert_eq!(editor.outlines[0].document, source);
assert_eq!(editor.active_outline().document.nodes().len(), 4);
}
@@ -7152,6 +7685,10 @@ mod tests {
let selection = editor.selection();
editor.insert(&mut engine, "x").unwrap();
assert_eq!(editor.outlines.len(), 1);
+ // A caret move ends the typing run, so the backspace is its own step.
+ for movement in [Movement::Left, Movement::Right] {
+ editor.move_selection(&mut engine, movement, false).unwrap();
+ }
editor.delete(&mut engine, true).unwrap();
assert!(editor.outlines.is_empty());
assert_eq!(editor.active_outline().document, spaces);
@@ -7368,6 +7905,9 @@ mod tests {
)
.unwrap();
editor.insert(&mut engine, "One").unwrap();
+ for movement in [Movement::Left, Movement::Right] {
+ editor.move_selection(&mut engine, movement, false).unwrap();
+ }
let source = editor.active_outline().snapshot();
let selection = editor.selection();
editor.insert(&mut engine, "!").unwrap();
diff --git a/crates/canvas/src/editor/evidence.rs b/crates/canvas/src/editor/evidence.rs
index 1b66ae6dbbb26dd82603d1fbf17ff612e5a7a8b4..19e44c28c7f1132dafb9bad94664402c0f54223f 100644
--- a/crates/canvas/src/editor/evidence.rs
+++ b/crates/canvas/src/editor/evidence.rs
@@ -91,13 +91,9 @@ fn open(engine: &mut TextEngine, summary: &str) -> CanvasEditor {
Some(label) => (label, true),
None => (tag, false),
};
- let tag = NoteTag::ALL
- .into_iter()
- .find(|tag| {
- matches!(&tag.definition().kind, Kind::TagDefinition { label: Some(name), .. } if name == label)
- })
- .unwrap();
- let definition = define(label.into(), tag.definition());
+ let defaults = NoteTag::defaults();
+ let place = defaults.iter().position(|tag| tag.label == label).unwrap();
+ let definition = define(label.into(), defaults[place].definition(place as u16));
node.text_mut().unwrap().tags.push(Tag {
definition: Some(definition),
action_type: None,
@@ -146,9 +142,12 @@ fn render(editor: &CanvasEditor) -> String {
};
let checked = if tag.status & 1 == 1 { "[x]" } else { "" };
(
- NoteTag::ALL
- .iter()
- .position(|known| known.definition().kind == *kind),
+ NoteTag::of(kind).and_then(|(tag, action_type)| {
+ NoteTag::defaults()
+ .iter()
+ .position(|known| *known == tag)
+ .filter(|place| *place == usize::from(action_type))
+ }),
format!("{}{checked}", label.as_deref().unwrap()),
)
})
diff --git a/crates/canvas/src/editor/format.rs b/crates/canvas/src/editor/format.rs
index 222e2181b7edf55fea5428592cf22c015e157614..bd5849f029408c8ea71a5bbf31f258ae1ecb0f88 100644
--- a/crates/canvas/src/editor/format.rs
+++ b/crates/canvas/src/editor/format.rs
@@ -5,6 +5,7 @@ use crate::layout::{DEFAULT_FONT, DEFAULT_FONT_SIZE};
use onestore::document::{Format, Kind, Tag};
use onestore::page::ParagraphContent;
use onestore::page::text::new_id;
+use serde::{Deserialize, Serialize};
/// The COLORREF OneNote stores for automatic colour, which overrides a style's colour.
const AUTOMATIC: u32 = 0xff00_0000;
@@ -32,7 +33,9 @@ pub enum Formatting {
Indent,
Outdent,
Align(Alignment),
- Tag(NoteTag),
+ /// A tag of the user's list and its place there, which the page stores as its action
+ /// type.
+ Tag(NoteTag, u16),
/// Removes every note tag from the selected paragraphs.
RemoveTags,
/// Checks the selected paragraphs' check boxes, or clears them when all are checked.
@@ -161,40 +164,102 @@ impl ListStyle {
}
}
-/// OneNote 2010's default tags, the first nine on Ctrl+1 to Ctrl+9, as it stores their
-/// definitions (`corpus/structural-probe/tag-gallery.one`); declared in gallery order, their
-/// action types.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum NoteTag {
- ToDo,
- Important,
- Question,
- RememberForLater,
- Definition,
- Highlight,
- Contact,
- Address,
- PhoneNumber,
- WebSiteToVisit,
- Idea,
- Password,
- Critical,
- ProjectA,
- ProjectB,
- MovieToSee,
- BookToRead,
- MusicToListenTo,
- SourceForArticle,
- RememberForBlog,
- DiscussWithPersonA,
- DiscussWithPersonB,
- DiscussWithManager,
- SendInEmail,
- ScheduleMeeting,
- CallBack,
- ToDoPriority1,
- ToDoPriority2,
- ClientRequest,
+/// A tag of the user's list as the Customize Tags dialog edits it: what its definition
+/// stores besides its place in the list, which the page stores as its action type.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct NoteTag {
+ pub label: String,
+ /// MS-ONE's NoteTagShape; 0 has no symbol.
+ pub shape: u16,
+ /// COLORREFs; none leaves the text's own.
+ pub color: Option,
+ pub highlight: Option,
+}
+
+/// OneNote 2010's tag list before it is customized, the first nine on Ctrl+1 to Ctrl+9
+/// (`corpus/structural-probe/tag-gallery.one`): names, symbols and highlights.
+const DEFAULT_TAGS: [(&str, u16, Option); 29] = [
+ ("To Do", CHECKBOX, None),
+ ("Important", 13, None),
+ ("Question", 15, None),
+ ("Remember for later", 0, Some(0x0000_ffff)),
+ ("Definition", 0, Some(0x0000_ff00)),
+ ("Highlight", 136, None),
+ ("Contact", 118, None),
+ ("Address", 23, None),
+ ("Phone number", 18, None),
+ ("Web site to visit", 125, None),
+ ("Idea", 21, None),
+ ("Password", 131, None),
+ ("Critical", 17, None),
+ ("Project A", 100, None),
+ ("Project B", 101, None),
+ ("Movie to see", 122, None),
+ ("Book to read", 132, None),
+ ("Music to listen to", 121, None),
+ ("Source for article", 125, None),
+ ("Remember for blog", 24, None),
+ ("Discuss with ", 94, None),
+ ("Discuss with ", 94, None),
+ ("Discuss with manager", 95, None),
+ ("Send in e-mail", 106, None),
+ ("Schedule meeting", 12, None),
+ ("Call back", 12, None),
+ ("To Do priority 1", 28, None),
+ ("To Do priority 2", 71, None),
+ ("Client request", 8, None),
+];
+
+impl NoteTag {
+ pub fn defaults() -> Vec {
+ DEFAULT_TAGS
+ .iter()
+ .map(|&(label, shape, highlight)| Self {
+ label: label.into(),
+ shape,
+ // The highlighting tags also set black text.
+ color: highlight.map(|_| 0),
+ highlight,
+ })
+ .collect()
+ }
+
+ /// The definition OneNote stores for the tag at place `action_type` in the list.
+ pub fn definition(&self, action_type: u16) -> Definition {
+ Definition {
+ kind: Kind::TagDefinition {
+ label: Some(self.label.clone()),
+ action_type: Some(action_type),
+ shape: Some(self.shape),
+ color: self.color,
+ highlight: self.highlight,
+ },
+ format: Format::default(),
+ }
+ }
+
+ /// The tag a stored definition describes, and its action type.
+ pub fn of(kind: &Kind) -> Option<(Self, u16)> {
+ let Kind::TagDefinition {
+ label,
+ action_type,
+ shape,
+ color,
+ highlight,
+ } = kind
+ else {
+ return None;
+ };
+ Some((
+ Self {
+ label: label.clone().unwrap_or_default(),
+ shape: shape.unwrap_or(0),
+ color: *color,
+ highlight: *highlight,
+ },
+ action_type.unwrap_or(0),
+ ))
+ }
}
/// What the selection shows on the toolbar.
@@ -212,8 +277,8 @@ pub struct FormatState {
pub numbering: bool,
/// The library style every selected paragraph's list shares.
pub list: Option,
- /// Tags every selected paragraph has.
- pub tags: Vec,
+ /// Tags every selected paragraph has, with their action types.
+ pub tags: Vec<(NoteTag, u16)>,
}
impl Toggle {
@@ -249,133 +314,6 @@ impl Toggle {
}
}
-impl NoteTag {
- pub const ALL: [Self; 29] = [
- Self::ToDo,
- Self::Important,
- Self::Question,
- Self::RememberForLater,
- Self::Definition,
- Self::Highlight,
- Self::Contact,
- Self::Address,
- Self::PhoneNumber,
- Self::WebSiteToVisit,
- Self::Idea,
- Self::Password,
- Self::Critical,
- Self::ProjectA,
- Self::ProjectB,
- Self::MovieToSee,
- Self::BookToRead,
- Self::MusicToListenTo,
- Self::SourceForArticle,
- Self::RememberForBlog,
- Self::DiscussWithPersonA,
- Self::DiscussWithPersonB,
- Self::DiscussWithManager,
- Self::SendInEmail,
- Self::ScheduleMeeting,
- Self::CallBack,
- Self::ToDoPriority1,
- Self::ToDoPriority2,
- Self::ClientRequest,
- ];
-
- /// The tag's name, as OneNote stores and lists it.
- pub const fn label(self) -> &'static str {
- match self {
- Self::ToDo => "To Do",
- Self::Important => "Important",
- Self::Question => "Question",
- Self::RememberForLater => "Remember for later",
- Self::Definition => "Definition",
- Self::Highlight => "Highlight",
- Self::Contact => "Contact",
- Self::Address => "Address",
- Self::PhoneNumber => "Phone number",
- Self::WebSiteToVisit => "Web site to visit",
- Self::Idea => "Idea",
- Self::Password => "Password",
- Self::Critical => "Critical",
- Self::ProjectA => "Project A",
- Self::ProjectB => "Project B",
- Self::MovieToSee => "Movie to see",
- Self::BookToRead => "Book to read",
- Self::MusicToListenTo => "Music to listen to",
- Self::SourceForArticle => "Source for article",
- Self::RememberForBlog => "Remember for blog",
- Self::DiscussWithPersonA => "Discuss with ",
- Self::DiscussWithPersonB => "Discuss with ",
- Self::DiscussWithManager => "Discuss with manager",
- Self::SendInEmail => "Send in e-mail",
- Self::ScheduleMeeting => "Schedule meeting",
- Self::CallBack => "Call back",
- Self::ToDoPriority1 => "To Do priority 1",
- Self::ToDoPriority2 => "To Do priority 2",
- Self::ClientRequest => "Client request",
- }
- }
-
- /// The symbol OneNote draws for the tag; 0 draws none.
- pub const fn shape(self) -> u16 {
- match self {
- Self::ToDo => CHECKBOX,
- Self::Important => 13,
- Self::Question => 15,
- Self::RememberForLater | Self::Definition => 0,
- Self::Highlight => 136,
- Self::Contact => 118,
- Self::Address => 23,
- Self::PhoneNumber => 18,
- Self::WebSiteToVisit | Self::SourceForArticle => 125,
- Self::Idea => 21,
- Self::Password => 131,
- Self::Critical => 17,
- Self::ProjectA => 100,
- Self::ProjectB => 101,
- Self::MovieToSee => 122,
- Self::BookToRead => 132,
- Self::MusicToListenTo => 121,
- Self::RememberForBlog => 24,
- Self::DiscussWithPersonA | Self::DiscussWithPersonB => 94,
- Self::DiscussWithManager => 95,
- Self::SendInEmail => 106,
- Self::ScheduleMeeting | Self::CallBack => 12,
- Self::ToDoPriority1 => 28,
- Self::ToDoPriority2 => 71,
- Self::ClientRequest => 8,
- }
- }
-
- pub(super) fn definition(self) -> Definition {
- let highlight = match self {
- Self::RememberForLater => Some(0x0000_ffff),
- Self::Definition => Some(0x0000_ff00),
- _ => None,
- };
- Definition {
- kind: Kind::TagDefinition {
- label: Some(self.label().into()),
- action_type: Some(self as u16),
- shape: Some(self.shape()),
- // The highlighting tags also set black text.
- color: highlight.map(|_| 0),
- highlight,
- },
- format: Format::default(),
- }
- }
-}
-
-/// Whether tag symbol `shape` is a check box, which OneNote stores unchecked when applied.
-fn checkable(shape: Option) -> bool {
- matches!(
- shape.and_then(|shape| crate::outline::TagIcon::of(shape, false)),
- Some(crate::outline::TagIcon::CheckBox { .. })
- )
-}
-
/// Where a selection starts and ends, as text leaf identity and byte offset.
type Ends = [(ExGuid, usize); 2];
@@ -664,15 +602,19 @@ impl CanvasEditor {
bullets: list(Formatting::Bullets),
numbering: list(Formatting::Numbering),
list: common(paragraphs.iter().map(|node| self.list_style(node))),
- tags: NoteTag::ALL
- .into_iter()
- .filter(|tag| {
- let kind = tag.definition().kind;
- paragraphs
- .iter()
- .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind)))
- })
- .collect(),
+ tags: paragraphs.first().map_or_else(Vec::new, |first| {
+ let mut shared: Vec<_> = tags(first)
+ .filter_map(|tag| NoteTag::of(self.tag_kind(tag)?))
+ .filter(|(tag, action_type)| {
+ let kind = tag.definition(*action_type).kind;
+ paragraphs
+ .iter()
+ .all(|node| tags(node).any(|tag| self.tag_kind(tag) == Some(&kind)))
+ })
+ .collect();
+ shared.sort_by_key(|(_, action_type)| *action_type);
+ shared
+ }),
})
}
@@ -702,7 +644,7 @@ impl CanvasEditor {
| Formatting::Bullets
| Formatting::Numbering
| Formatting::List(_)
- | Formatting::Tag(_)
+ | Formatting::Tag(..)
| Formatting::RemoveTags
| Formatting::Check
if title =>
@@ -783,14 +725,8 @@ impl CanvasEditor {
}
});
}
- Formatting::Tag(tag) => {
- let definition = tag.definition();
- let Kind::TagDefinition {
- action_type, shape, ..
- } = definition.kind
- else {
- unreachable!()
- };
+ Formatting::Tag(tag, action_type) => {
+ let definition = tag.definition(*action_type);
let existing = self
.definitions
.iter()
@@ -821,12 +757,12 @@ impl CanvasEditor {
Some(kind) if remove => *kind != definition.kind,
Some(Kind::TagDefinition {
action_type: other, ..
- }) => *other != action_type,
+ }) => *other != Some(*action_type),
_ => true,
});
}
if !remove {
- let checkable = checkable(shape);
+ let checkable = crate::outline::checkable(tag.shape);
// Stored newest first.
text.tags.insert(
0,
@@ -967,7 +903,7 @@ impl CanvasEditor {
Formatting::Toggle(toggle) => state.toggles.contains(toggle),
Formatting::Bullets => state.bullets,
Formatting::Numbering => state.numbering,
- Formatting::Tag(tag) => state.tags.contains(tag),
+ Formatting::Tag(tag, action_type) => state.tags.contains(&(tag.clone(), *action_type)),
_ => false,
};
let mut outlines = Vec::new();
@@ -1038,7 +974,8 @@ impl CanvasEditor {
let checkable = |tag: &Tag| {
matches!(
self.tag_kind(tag),
- Some(Kind::TagDefinition { shape, .. }) if checkable(*shape)
+ Some(Kind::TagDefinition { shape: Some(shape), .. })
+ if crate::outline::checkable(*shape)
)
};
let checked = self::covered(replacement, ends)
@@ -1823,7 +1760,10 @@ mod tests {
let mut editor = plain(&mut engine, &["task", "other"]);
editor.select([at(0, 0), at(1, 5)].into()).unwrap();
editor
- .format(&mut engine, Formatting::Tag(NoteTag::ToDo))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
+ )
.unwrap();
editor.select([at(1, 2); 2].into()).unwrap();
let outline = editor.active_outline().id;
@@ -1886,27 +1826,33 @@ mod tests {
let original = editor.active_outline().document.clone();
editor.select([at(0, 1), at(1, 2)].into()).unwrap();
editor
- .format(&mut engine, Formatting::Tag(NoteTag::ToDo))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
+ )
.unwrap();
assert_eq!(text_tags(&editor, 0), [("To Do".into(), 0, true)]);
assert_eq!(text_tags(&editor, 1), [("To Do".into(), 0, true)]);
- assert_eq!(editor.format_state().unwrap().tags, [NoteTag::ToDo]);
+ assert_eq!(
+ editor.format_state().unwrap().tags,
+ [(NoteTag::defaults()[0].clone(), 0)]
+ );
let icon =
|editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon;
assert_eq!(
icon(&editor),
- crate::outline::TagIcon::CheckBox {
- checked: false,
- mark: None
+ crate::outline::TagIcon::Symbol {
+ shape: 3,
+ checked: false
}
);
editor.format(&mut engine, Formatting::Check).unwrap();
assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]);
assert_eq!(
icon(&editor),
- crate::outline::TagIcon::CheckBox {
- checked: true,
- mark: None
+ crate::outline::TagIcon::Symbol {
+ shape: 3,
+ checked: true
}
);
editor.select([at(1, 0); 2].into()).unwrap();
@@ -1918,27 +1864,39 @@ mod tests {
editor.select([at(1, 0), at(2, 5)].into()).unwrap();
editor
- .format(&mut engine, Formatting::Tag(NoteTag::Question))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
+ )
.unwrap();
assert_eq!(
text_tags(&editor, 1),
[("Question".into(), 1, true), ("To Do".into(), 1, true)]
);
assert_eq!(text_tags(&editor, 2), [("Question".into(), 1, true)]);
- assert_eq!(editor.format_state().unwrap().tags, [NoteTag::Question]);
+ assert_eq!(
+ editor.format_state().unwrap().tags,
+ [(NoteTag::defaults()[2].clone(), 2)]
+ );
let questions = editor
.definitions
.values()
- .filter(|definition| **definition == NoteTag::Question.definition())
+ .filter(|definition| **definition == NoteTag::defaults()[2].definition(2))
.count();
assert_eq!(questions, 1);
editor
- .format(&mut engine, Formatting::Tag(NoteTag::Question))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
+ )
.unwrap();
assert_eq!(text_tags(&editor, 2), []);
editor.select([at(0, 0); 2].into()).unwrap();
editor
- .format(&mut engine, Formatting::Tag(NoteTag::ToDo))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
+ )
.unwrap();
assert_eq!(text_tags(&editor, 0), []);
for _ in 0..7 {
@@ -1961,44 +1919,35 @@ mod tests {
.values()
.filter(|definition| matches!(definition.kind, Kind::TagDefinition { .. }))
.collect::>();
- assert_eq!(stored.len(), NoteTag::ALL.len());
- for tag in NoteTag::ALL {
- assert!(stored.contains(&&tag.definition()), "{tag:?}");
+ let defaults = NoteTag::defaults();
+ assert_eq!(stored.len(), defaults.len());
+ for (place, tag) in defaults.iter().enumerate() {
+ assert!(stored.contains(&&tag.definition(place as u16)), "{tag:?}");
}
let mut engine = TextEngine::default();
let mut editor = plain(&mut engine, &["tagged"]);
- for tag in NoteTag::ALL {
- editor.format(&mut engine, Formatting::Tag(tag)).unwrap();
+ for (place, tag) in defaults.iter().enumerate() {
+ editor
+ .format(&mut engine, Formatting::Tag(tag.clone(), place as u16))
+ .unwrap();
}
- assert_eq!(editor.format_state().unwrap().tags, NoteTag::ALL);
+ assert_eq!(editor.format_state().unwrap().tags.len(), defaults.len());
let paragraph = &editor.active_outline().shaped.paragraphs[0];
- use crate::outline::TagIcon;
- let icons = paragraph
+ let shapes = paragraph
.tags
.iter()
- .map(|tag| tag.icon)
+ .map(|tag| match tag.icon {
+ crate::outline::TagIcon::Symbol { shape, .. } => shape,
+ crate::outline::TagIcon::Task => unreachable!(),
+ })
.collect::>();
- assert_eq!(icons.len(), NoteTag::ALL.len() - 2);
- assert_eq!(
- icons[..7],
- [
- TagIcon::CheckBox {
- checked: false,
- mark: None
- },
- TagIcon::Star,
- TagIcon::Question,
- TagIcon::Highlight,
- TagIcon::Contact,
- TagIcon::Address,
- TagIcon::Phone,
- ]
- );
+ assert_eq!(shapes.len(), defaults.len() - 2);
+ assert_eq!(shapes[..7], [3, 13, 15, 136, 118, 23, 18]);
// As OneNote stores them unchecked: To Do, Discuss with, Schedule meeting, Call back,
// the priorities and Client request.
- let checkable = NoteTag::ALL
- .into_iter()
- .filter(|tag| checkable(Some(tag.shape())))
+ let checkable = defaults
+ .iter()
+ .filter(|tag| crate::outline::checkable(tag.shape))
.count();
assert_eq!(checkable, 9);
// Remember for later and Definition draw no symbol; the newer one's green marks the text.
@@ -2011,6 +1960,46 @@ mod tests {
assert!(paragraph.text.backgrounds().next().is_some());
}
+ /// A customized tag stores its name, symbol and colours with its place in the list as
+ /// its action type; applied from another place it takes a definition of its own, as
+ /// OneNote 2010's does (`corpus/custom-tags`).
+ #[test]
+ fn custom_tags_store_their_look_and_place() {
+ let mut engine = TextEngine::default();
+ let mut editor = plain(&mut engine, &["one", "two"]);
+ let tag = NoteTag {
+ label: "Snow check".into(),
+ shape: 61,
+ color: Some(0x0000_0080),
+ highlight: Some(0x00ff_cc00),
+ };
+ editor
+ .format(&mut engine, Formatting::Tag(tag.clone(), 0))
+ .unwrap();
+ assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 0)]);
+ assert_eq!(text_tags(&editor, 0), [("Snow check".into(), 1, true)]);
+ editor.select([at(1, 0); 2].into()).unwrap();
+ editor
+ .format(&mut engine, Formatting::Tag(tag.clone(), 1))
+ .unwrap();
+ assert_eq!(editor.format_state().unwrap().tags, [(tag.clone(), 1)]);
+ let mut stored: Vec<_> = editor
+ .definitions
+ .values()
+ .filter_map(|definition| NoteTag::of(&definition.kind))
+ .collect();
+ stored.sort_by_key(|(_, action_type)| *action_type);
+ assert_eq!(stored, [(tag.clone(), 0), (tag, 1)]);
+ let paragraph = &editor.active_outline().shaped.paragraphs[0];
+ assert!(
+ paragraph
+ .text
+ .backgrounds()
+ .all(|(_, color)| color == 0x00ff_cc00)
+ );
+ assert!(paragraph.text.backgrounds().next().is_some());
+ }
+
#[test]
fn commands_reach_every_cell_a_selection_crosses() {
let mut engine = TextEngine::default();
@@ -2049,7 +2038,10 @@ mod tests {
editor.format(&mut engine, Formatting::Numbering).unwrap();
assert_eq!(numbers(&mut engine, &editor), [None, Some(1)]);
editor
- .format(&mut engine, Formatting::Tag(NoteTag::ToDo))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
+ )
.unwrap();
assert_eq!(editor.active_outline().shaped.paragraphs[1].tags.len(), 1);
assert_eq!(editor.selection(), Selection::from([at(1, 0); 2]));
@@ -2110,8 +2102,8 @@ mod tests {
Formatting::Highlight(Some(0x0000_ffff)),
Formatting::Align(Alignment::Center),
Formatting::Numbering,
- Formatting::Tag(NoteTag::ToDo),
- Formatting::Tag(NoteTag::Question),
+ Formatting::Tag(NoteTag::defaults()[0].clone(), 0),
+ Formatting::Tag(NoteTag::defaults()[2].clone(), 2),
Formatting::Check,
] {
editor.format(&mut engine, command).unwrap();
@@ -2136,7 +2128,10 @@ mod tests {
bullets: false,
numbering: true,
list: Some(ListStyle::NUMBER),
- tags: vec![NoteTag::ToDo, NoteTag::Question],
+ tags: vec![
+ (NoteTag::defaults()[0].clone(), 0),
+ (NoteTag::defaults()[2].clone(), 2)
+ ],
}
);
// Format Painter, a gallery tag and an inserted table write as well.
@@ -2155,7 +2150,10 @@ mod tests {
)
);
editor
- .format(&mut engine, Formatting::Tag(NoteTag::CallBack))
+ .format(
+ &mut engine,
+ Formatting::Tag(NoteTag::defaults()[25].clone(), 25),
+ )
.unwrap();
editor.select([at(0, 1); 2].into()).unwrap();
editor.insert_table(&mut engine, 2, 3).unwrap();
diff --git a/crates/canvas/src/editor/ops.rs b/crates/canvas/src/editor/ops.rs
index 3b12789442eaaf253b8183ca31427b8fcb319613..4bbd1fa473e2ba33845526379612b00b7fbb29c5 100644
--- a/crates/canvas/src/editor/ops.rs
+++ b/crates/canvas/src/editor/ops.rs
@@ -330,6 +330,9 @@ impl CanvasEditor {
page::Content::Ink(ink) if ink.id == placement.id => {
return layout_ops(ink.id, &old, &moved(&ink.layout));
}
+ page::Content::File { source, .. } if source.id == placement.id => {
+ return layout_ops(source.id, &old, &moved(&source.layout));
+ }
page::Content::Outline { source, .. } if source.id == placement.id => {
return layout_ops(source.id, &old, &moved(&source.layout));
}
@@ -434,6 +437,7 @@ impl CanvasEditor {
page::Content::Outline { source, .. } => source.id,
page::Content::Date { .. } => continue,
page::Content::Image(image) => image.id,
+ page::Content::File { source, .. } => source.id,
page::Content::Ink(ink) => ink.id,
page::Content::ReadOnly(object)
if matches!(object.source, PageObject::Title(_)) =>
diff --git a/crates/canvas/src/editor/page.rs b/crates/canvas/src/editor/page.rs
index a90a91a003cc6ed0f9ec986a51272bdf818f231d..7b7def205020a6a3b7a45aa5e15e7c10c56a8502 100644
--- a/crates/canvas/src/editor/page.rs
+++ b/crates/canvas/src/editor/page.rs
@@ -5,7 +5,7 @@ use crate::{
outline::{Arrange, OutlineLayout},
};
use onestore::page::text::Paragraph;
-use onestore::page::{Image, Ink, Outline, Page, PageObject};
+use onestore::page::{Attachment, Image, Ink, Outline, Page, PageObject};
use std::collections::BTreeMap;
/// A title object's own state, plus the child origins `build` replaces with page coordinates.
@@ -33,6 +33,11 @@ pub(crate) enum Content {
below_title: Option,
},
Image(Image),
+ /// A file on the page, laid out relative to its position.
+ File {
+ source: Attachment,
+ layout: Box,
+ },
Ink(Ink),
Editable(onestore::ExGuid),
ReadOnly(Box),
@@ -60,22 +65,46 @@ impl Content {
)?))
}
+ /// The page-level picture shown, drawn or as a placeholder.
+ pub(super) fn picture(&self) -> Option<&Image> {
+ match self {
+ Self::Image(image) => Some(image),
+ Self::ReadOnly(object) => match &object.source {
+ PageObject::Image(image) => Some(image),
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+
pub(super) fn layout(&self) -> Option<(onestore::ExGuid, &onestore::document::Layout)> {
match self {
Self::Outline { source, .. } => Some((source.id, &source.layout)),
Self::Image(source) => Some((source.id, &source.layout)),
+ Self::File { source, .. } => Some((source.id, &source.layout)),
Self::Ink(source) => Some((source.id, &source.layout)),
Self::ReadOnly(object) => Some((object.source.id(), object.source.layout())),
Self::Date { .. } | Self::Editable(_) => None,
}
}
+ /// A file on the page, where its column's `[x0, y0, x1, y1]` lie in page points.
+ pub(crate) fn file(&self) -> Option<(&Attachment, [f32; 4])> {
+ let Self::File { source, layout } = self else {
+ return None;
+ };
+ let [x, y] = [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0));
+ let [x0, y0, x1, y1] = layout.bounds();
+ Some((source, [x + x0, y + y0, x + x1, y + y1]))
+ }
+
pub(super) fn layout_mut(
&mut self,
) -> Option<(onestore::ExGuid, &mut onestore::document::Layout)> {
match self {
Self::Outline { source, .. } => Some((source.id, &mut source.layout)),
Self::Image(source) => Some((source.id, &mut source.layout)),
+ Self::File { source, .. } => Some((source.id, &mut source.layout)),
Self::Ink(source) => Some((source.id, &mut source.layout)),
Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())),
Self::Date { .. } | Self::Editable(_) => None,
@@ -332,6 +361,19 @@ pub(crate) fn build(
};
objects.push(Content::Image(source));
}
+ PageObject::Attachment(source) => {
+ if [source.layout.x, source.layout.y]
+ .iter()
+ .any(|v| !v.unwrap_or(0.0).is_finite())
+ {
+ return Err(EditorError::InvalidGeometry);
+ }
+ let layout = Box::new(crate::outline::page_file(engine, source)?);
+ let PageObject::Attachment(source) = object else {
+ unreachable!()
+ };
+ objects.push(Content::File { source, layout });
+ }
PageObject::Ink(ink) => {
if ink_bounds(ink).is_some_and(|b| b.iter().any(|v| !v.is_finite())) {
return Err(EditorError::InvalidGeometry);
diff --git a/crates/canvas/src/editor/space.rs b/crates/canvas/src/editor/space.rs
index 5e4a9c13aa74998baa34899594c8f3fba80b2d6c..0b5de50edeb65abfc58b5e5bf49e462728b9203a 100644
--- a/crates/canvas/src/editor/space.rs
+++ b/crates/canvas/src/editor/space.rs
@@ -5,7 +5,7 @@ use super::*;
/// Something Insert Space moves.
enum Piece {
- /// A body outline, picture or ink drawing, placed anew.
+ /// A body outline, picture, file or ink drawing, placed anew.
Object(ExGuid),
/// The root nodes of a body outline from `at` on.
Tail { outline: ExGuid, at: usize },
@@ -81,6 +81,7 @@ impl CanvasEditor {
];
(image.id, rect)
}
+ page::Content::File { source, .. } => (source.id, content.file().unwrap().1),
page::Content::Ink(ink) => match page::ink_bounds(ink) {
Some(rect) => (ink.id, rect),
None => continue,
diff --git a/crates/canvas/src/gpu/mod.rs b/crates/canvas/src/gpu/mod.rs
index 34357767a6e35a2022fc831c57e1a7cc93af6743..11694f2fb8d7abeaefe92b7cc1255de7913e54c0 100644
--- a/crates/canvas/src/gpu/mod.rs
+++ b/crates/canvas/src/gpu/mod.rs
@@ -4,7 +4,7 @@ mod profile;
use crate::{
layout::{TextBrush, TextLayout},
- outline::{BoxMark, TagIcon},
+ outline::TagIcon,
};
use draw::{GlyphRun, Glyphs, Layer, Primitive, RenderError};
use parley::PositionedLayoutItem;
@@ -140,116 +140,199 @@ pub(crate) fn painted_layout(text: &dyn Glyphs) -> &TextLayout {
.expect("Page text is a TextLayout")
}
-const CHECKBOX: &str = include_str!("../../assets/tags/checkbox.svg");
const CHECKMARK: &str = include_str!("../../assets/tags/checkmark.svg");
-const STAR: &str = include_str!("../../assets/tags/star.svg");
-const QUESTION: &str = include_str!("../../assets/tags/question.svg");
-const HIGHLIGHT: &str = include_str!("../../assets/tags/highlight.svg");
-const CONTACT: &str = include_str!("../../assets/tags/contact.svg");
-const ADDRESS: &str = include_str!("../../assets/tags/address.svg");
-const PHONE: &str = include_str!("../../assets/tags/phone.svg");
-const MUSIC: &str = include_str!("../../assets/tags/music.svg");
-const EXCLAMATION: &str = include_str!("../../assets/tags/exclamation.svg");
-const RED_SQUARE: &str = include_str!("../../assets/tags/red-square.svg");
-const YELLOW_SQUARE: &str = include_str!("../../assets/tags/yellow-square.svg");
-const BLUE_SQUARE: &str = include_str!("../../assets/tags/blue-square.svg");
const FLAG: &str = include_str!("../../assets/tags/flag.svg");
-const IDEA: &str = include_str!("../../assets/tags/idea.svg");
-const PASSWORD: &str = include_str!("../../assets/tags/password.svg");
-const MOVIE: &str = include_str!("../../assets/tags/movie.svg");
-const BOOK: &str = include_str!("../../assets/tags/book.svg");
-const WEB: &str = include_str!("../../assets/tags/web.svg");
-const BLOG: &str = include_str!("../../assets/tags/blog.svg");
-const EMAIL: &str = include_str!("../../assets/tags/email.svg");
const BOX_SMALL: &str = include_str!("../../assets/tags/box-small.svg");
const CHECK_SMALL: &str = include_str!("../../assets/tags/check-small.svg");
-const MARK_PERSON: &str = include_str!("../../assets/tags/mark-person.svg");
-const MARK_MANAGER: &str = include_str!("../../assets/tags/mark-manager.svg");
-const MARK_ARROW: &str = include_str!("../../assets/tags/mark-arrow.svg");
-const MARK_ONE: &str = include_str!("../../assets/tags/mark-one.svg");
-const MARK_TWO: &str = include_str!("../../assets/tags/mark-two.svg");
-const MARK_CLIENT: &str = include_str!("../../assets/tags/mark-client.svg");
const TAG: &str = include_str!("../../assets/tags/tag.svg");
+/// How a tag symbol's artwork takes a check: a large box under it, a small box with the
+/// check between the box and its badge, or art that shows none.
+enum Art {
+ Box(&'static str),
+ Badged(&'static str),
+ Plain(&'static str),
+}
+
+/// The artwork of symbol `shape` of MS-ONE's NoteTagShape, as OneNote 2010 draws it on the
+/// page: it draws 54, Green 8-Point Star, blue there. A shape MS-ONE does not list shows a
+/// plain tag.
+fn art(shape: u16) -> Art {
+ use Art::{Badged, Box, Plain};
+ macro_rules! tag {
+ ($name:literal) => {
+ include_str!(concat!("../../assets/tags/", $name, ".svg"))
+ };
+ }
+ match shape {
+ 1 => Box(tag!("checkbox-green")),
+ 2 => Box(tag!("checkbox-yellow")),
+ 3 => Box(tag!("checkbox")),
+ 4 => Badged(tag!("mark-star-green")),
+ 5 => Badged(tag!("mark-star-yellow")),
+ 6 => Badged(tag!("mark-star-blue")),
+ 7 => Badged(tag!("mark-exclamation-green")),
+ 8 => Badged(tag!("mark-exclamation-yellow")),
+ 9 => Badged(tag!("mark-exclamation-blue")),
+ 10 => Badged(tag!("mark-arrow-green")),
+ 11 => Badged(tag!("mark-arrow-yellow")),
+ 12 => Badged(tag!("mark-arrow-blue")),
+ 13 => Plain(tag!("star")),
+ 14 => Plain(tag!("follow-up")),
+ 15 => Plain(tag!("question")),
+ 16 => Plain(tag!("arrow-right-blue")),
+ 17 => Plain(tag!("exclamation")),
+ 18 => Plain(tag!("phone")),
+ 19 => Plain(tag!("calendar")),
+ 20 => Plain(tag!("clock")),
+ 21 => Plain(tag!("idea")),
+ 22 => Plain(tag!("pushpin")),
+ 23 => Plain(tag!("address")),
+ 24 => Plain(tag!("blog")),
+ 25 => Plain(tag!("smiley")),
+ 26 => Plain(tag!("ribbon")),
+ 27 => Plain(tag!("key")),
+ 28 => Badged(tag!("mark-one-blue")),
+ 29 => Plain(tag!("circle-1-blue")),
+ 30 => Badged(tag!("mark-two-blue")),
+ 31 => Plain(tag!("circle-2-blue")),
+ 32 => Badged(tag!("mark-three-blue")),
+ 33 => Plain(tag!("circle-3-blue")),
+ 34 => Plain(tag!("star8-blue")),
+ 35 => Plain(tag!("tick-blue")),
+ 36 => Plain(tag!("circle-blue")),
+ 37 => Plain(tag!("arrow-down-blue")),
+ 38 => Plain(tag!("arrow-left-blue")),
+ 39 => Plain(tag!("solid-target-blue")),
+ 40 => Plain(tag!("star-blue")),
+ 41 => Plain(tag!("sun-blue")),
+ 42 => Plain(tag!("target-blue")),
+ 43 => Plain(tag!("triangle-blue")),
+ 44 => Plain(tag!("umbrella-blue")),
+ 45 => Plain(tag!("arrow-up-blue")),
+ 46 => Plain(tag!("x-dots-blue")),
+ 47 => Plain(tag!("x-blue")),
+ 48 => Badged(tag!("mark-one-green")),
+ 49 => Plain(tag!("circle-1-green")),
+ 50 => Badged(tag!("mark-two-green")),
+ 51 => Plain(tag!("circle-2-green")),
+ 52 => Badged(tag!("mark-three-green")),
+ 53 => Plain(tag!("circle-3-green")),
+ 54 => Plain(tag!("star8-blue")),
+ 55 => Plain(tag!("tick-green")),
+ 56 => Plain(tag!("circle-green")),
+ 57 => Plain(tag!("arrow-down-green")),
+ 58 => Plain(tag!("arrow-left-green")),
+ 59 => Plain(tag!("arrow-right-green")),
+ 60 => Plain(tag!("solid-target-green")),
+ 61 => Plain(tag!("star-green")),
+ 62 => Plain(tag!("sun-green")),
+ 63 => Plain(tag!("target-green")),
+ 64 => Plain(tag!("triangle-green")),
+ 65 => Plain(tag!("umbrella-green")),
+ 66 => Plain(tag!("arrow-up-green")),
+ 67 => Plain(tag!("x-dots-green")),
+ 68 => Plain(tag!("x-green")),
+ 69 => Badged(tag!("mark-one-yellow")),
+ 70 => Plain(tag!("circle-1-yellow")),
+ 71 => Badged(tag!("mark-two-yellow")),
+ 72 => Plain(tag!("circle-2-yellow")),
+ 73 => Badged(tag!("mark-three-yellow")),
+ 74 => Plain(tag!("circle-3-yellow")),
+ 75 => Plain(tag!("star8-yellow")),
+ 76 => Plain(tag!("tick-yellow")),
+ 77 => Plain(tag!("circle-yellow")),
+ 78 => Plain(tag!("arrow-down-yellow")),
+ 79 => Plain(tag!("arrow-left-yellow")),
+ 80 => Plain(tag!("arrow-right-yellow")),
+ 81 => Plain(tag!("solid-target-yellow")),
+ 82 => Plain(tag!("sun-yellow")),
+ 83 => Plain(tag!("target-yellow")),
+ 84 => Plain(tag!("triangle-yellow")),
+ 85 => Plain(tag!("umbrella-yellow")),
+ 86 => Plain(tag!("arrow-up-yellow")),
+ 87 => Plain(tag!("x-dots-yellow")),
+ 88 => Plain(tag!("x-yellow")),
+ 89 => Plain(tag!("flag-today")),
+ 90 => Plain(tag!("flag-tomorrow")),
+ 91 => Plain(tag!("flag-this-week")),
+ 92 => Plain(tag!("flag-next-week")),
+ 93 => Plain(tag!("flag-no-date")),
+ 94 => Badged(tag!("mark-person-blue")),
+ 95 => Badged(tag!("mark-person-yellow")),
+ 96 => Badged(tag!("mark-person-green")),
+ 97 => Badged(tag!("mark-flag-blue")),
+ 98 => Badged(tag!("mark-flag-yellow")),
+ 99 => Badged(tag!("mark-flag-green")),
+ 100 => Plain(tag!("red-square")),
+ 101 => Plain(tag!("yellow-square")),
+ 102 => Plain(tag!("blue-square")),
+ 103 => Plain(tag!("green-square")),
+ 104 => Plain(tag!("orange-square")),
+ 105 => Plain(tag!("pink-square")),
+ 106 => Plain(tag!("email")),
+ 107 => Plain(tag!("envelope")),
+ 108 => Plain(tag!("envelope-open")),
+ 109 => Plain(tag!("mobile")),
+ 110 => Plain(tag!("phone-clock")),
+ 111 => Plain(tag!("question-balloon")),
+ 112 => Plain(tag!("paperclip")),
+ 113 => Plain(tag!("frown")),
+ 114 => Plain(tag!("im-contact")),
+ 115 => Plain(tag!("person")),
+ 116 => Plain(tag!("people")),
+ 117 => Plain(tag!("bell")),
+ 118 => Plain(tag!("contact")),
+ 119 => Plain(tag!("rose")),
+ 120 => Plain(tag!("date")),
+ 121 => Plain(tag!("music")),
+ 122 => Plain(tag!("movie")),
+ 123 => Plain(tag!("quote")),
+ 124 => Plain(tag!("globe")),
+ 125 => Plain(tag!("web")),
+ 126 => Plain(tag!("laptop")),
+ 127 => Plain(tag!("plane")),
+ 128 => Plain(tag!("car")),
+ 129 => Plain(tag!("binoculars")),
+ 130 => Plain(tag!("presentation")),
+ 131 => Plain(tag!("password")),
+ 132 => Plain(tag!("book")),
+ 133 => Plain(tag!("notebook")),
+ 134 => Plain(tag!("paper")),
+ 135 => Plain(tag!("research")),
+ 136 => Plain(tag!("highlight")),
+ 137 => Plain(tag!("dollar")),
+ 138 => Plain(tag!("coins")),
+ 139 => Plain(tag!("schedule")),
+ 140 => Plain(tag!("lightning")),
+ 141 => Plain(tag!("cloud")),
+ 142 => Plain(tag!("heart")),
+ 143 => Plain(tag!("sunflower")),
+ _ => Plain(TAG),
+ }
+}
+
+/// Symbols MS-ONE lists.
+const SYMBOLS: u16 = 143;
+
/// The tag's artwork, drawn in order.
pub fn tag_sources(icon: TagIcon) -> &'static [&'static str] {
- match icon {
- TagIcon::CheckBox {
- checked: false,
- mark: None,
- } => &[CHECKBOX],
- TagIcon::CheckBox {
- checked: true,
- mark: None,
- } => &[CHECKBOX, CHECKMARK],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::Person),
- } => &[BOX_SMALL, MARK_PERSON],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::Person),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_PERSON],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::Manager),
- } => &[BOX_SMALL, MARK_MANAGER],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::Manager),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_MANAGER],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::Arrow),
- } => &[BOX_SMALL, MARK_ARROW],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::Arrow),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_ARROW],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::One),
- } => &[BOX_SMALL, MARK_ONE],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::One),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_ONE],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::Two),
- } => &[BOX_SMALL, MARK_TWO],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::Two),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_TWO],
- TagIcon::CheckBox {
- checked: false,
- mark: Some(BoxMark::Client),
- } => &[BOX_SMALL, MARK_CLIENT],
- TagIcon::CheckBox {
- checked: true,
- mark: Some(BoxMark::Client),
- } => &[BOX_SMALL, CHECK_SMALL, MARK_CLIENT],
- TagIcon::Star => &[STAR],
- TagIcon::Question => &[QUESTION],
- TagIcon::Highlight => &[HIGHLIGHT],
- TagIcon::Contact => &[CONTACT],
- TagIcon::Address => &[ADDRESS],
- TagIcon::Phone => &[PHONE],
- TagIcon::Music => &[MUSIC],
- TagIcon::Exclamation => &[EXCLAMATION],
- TagIcon::RedSquare => &[RED_SQUARE],
- TagIcon::YellowSquare => &[YELLOW_SQUARE],
- TagIcon::BlueSquare => &[BLUE_SQUARE],
- TagIcon::Flag => &[FLAG],
- TagIcon::Idea => &[IDEA],
- TagIcon::Password => &[PASSWORD],
- TagIcon::Movie => &[MOVIE],
- TagIcon::Book => &[BOOK],
- TagIcon::Web => &[WEB],
- TagIcon::Blog => &[BLOG],
- TagIcon::Email => &[EMAIL],
- TagIcon::Other => &[TAG],
+ static SOURCES: std::sync::OnceLock; 2]>> = std::sync::OnceLock::new();
+ let TagIcon::Symbol { shape, checked } = icon else {
+ return &[FLAG];
+ };
+ let sources = SOURCES.get_or_init(|| {
+ (0..=SYMBOLS)
+ .map(|shape| match art(shape) {
+ Art::Box(art) => [vec![art], vec![art, CHECKMARK]],
+ Art::Badged(badge) => [vec![BOX_SMALL, badge], vec![BOX_SMALL, CHECK_SMALL, badge]],
+ Art::Plain(art) => [vec![art], vec![art]],
+ })
+ .collect()
+ });
+ match sources.get(usize::from(shape)) {
+ Some(sources) => &sources[usize::from(checked)],
+ None => &[TAG],
}
}
@@ -364,29 +447,26 @@ mod tests {
mapped_at_creation: false,
});
let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb);
- let icons = [
- TagIcon::CheckBox {
- checked: false,
- mark: None,
- },
- TagIcon::CheckBox {
- checked: true,
- mark: None,
- },
- TagIcon::Star,
- TagIcon::Question,
- TagIcon::Highlight,
- TagIcon::Contact,
- TagIcon::Address,
- TagIcon::Phone,
- TagIcon::Music,
- TagIcon::Exclamation,
- TagIcon::RedSquare,
- TagIcon::YellowSquare,
- TagIcon::BlueSquare,
- TagIcon::Flag,
- TagIcon::Other,
- ];
+ let icons: Vec<_> = [
+ (3, false),
+ (3, true),
+ (13, false),
+ (15, false),
+ (136, false),
+ (118, false),
+ (23, false),
+ (18, false),
+ (121, false),
+ (17, false),
+ (100, false),
+ (101, false),
+ (102, false),
+ (999, false),
+ ]
+ .into_iter()
+ .map(|(shape, checked)| TagIcon::of(shape, checked).unwrap())
+ .chain([TagIcon::Task])
+ .collect();
let text = TextEngine::default()
.layout(
&Paragraph::from_runs([
diff --git a/crates/canvas/src/gpu/page.rs b/crates/canvas/src/gpu/page.rs
index 3d1f632f8db485567f5015efcf87e0a46aa20552..2c4cdd47853e46e6333419516c432bf8d8922d61 100644
--- a/crates/canvas/src/gpu/page.rs
+++ b/crates/canvas/src/gpu/page.rs
@@ -79,6 +79,7 @@ pub enum SceneHit {
Date(DateField),
ReadOnly(usize),
Image(onestore::ExGuid),
+ File(onestore::ExGuid),
}
fn outline_origin(
@@ -139,6 +140,14 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec<
}
}
+const FILE_ICON: &str = include_str!("../../assets/file.svg");
+
+/// The blank page for a file without an icon of its own, as the 32 pixel PNG OneNote 2010
+/// stores with every attachment; it draws a broken picture for one stored without.
+pub fn file_icon() -> Vec {
+ background::png(FILE_ICON, 32)
+}
+
fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec>) {
primitives.push(Primitive::Rect {
rect,
@@ -230,10 +239,9 @@ impl PageScene {
}
}
for (id, art) in &mut scene.backgrounds {
- if let Some(shown) = self.backgrounds.remove(id)
- && shown.same(art)
- {
- *art = shown;
+ match self.backgrounds.remove(id) {
+ Some(shown) if shown.same(art) => *art = shown,
+ _ => art.inline = true,
}
}
scene.mark_unavailable(&mut editor.objects, engine)?;
@@ -241,6 +249,28 @@ impl PageScene {
Ok(())
}
+ /// Follows `editor` after an edit or undo brought page-level pictures the scene has not
+ /// seen, as `refresh` does.
+ pub fn follow(
+ &mut self,
+ editor: &mut CanvasEditor,
+ engine: &mut TextEngine,
+ ) -> Result<(), SceneError> {
+ let known = |object: &Content| match object {
+ Content::Image(source) => {
+ self.pictures.contains_key(&source.id) || self.backgrounds.contains_key(&source.id)
+ }
+ Content::File { source, .. } => {
+ source.preview.is_none() || self.pictures.contains_key(&source.id)
+ }
+ _ => true,
+ };
+ if editor.objects.iter().all(known) {
+ return Ok(());
+ }
+ self.refresh(editor, engine)
+ }
+
/// Turns page pictures the renderer cannot decode into placeholders; their stored data
/// is kept.
fn mark_unavailable(
@@ -275,7 +305,6 @@ impl PageScene {
onestore::page::ParagraphContent::Image(image) => {
payloads.push((image.id, image.bytes.as_ref()))
}
- // A file without the icon OneNote rendered for it keeps an empty slot.
onestore::page::ParagraphContent::Attachment(file) => {
if let Some(icon) = file.preview.as_ref() {
payloads.push((file.id, Some(icon)))
@@ -303,6 +332,11 @@ impl PageScene {
payloads.push((source.id, source.bytes.as_ref()))
}
}
+ Content::File { source, .. } => {
+ if let Some(icon) = source.preview.as_ref() {
+ payloads.push((source.id, Some(icon)))
+ }
+ }
Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads),
Content::Editable(id) => {
if let Some(outline) =
@@ -396,6 +430,11 @@ impl PageScene {
rects.push((source.id, [x, y, x + width, y + height]));
}
}
+ Content::File { source, layout } => {
+ let [x, y] = [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0));
+ let [x0, y0, x1, y1] = layout.rect;
+ rects.push((source.id, [x + x0, y + y0, x + x1, y + y1]));
+ }
Content::Outline {
source,
layout,
@@ -433,40 +472,61 @@ impl PageScene {
primitives: &mut Vec>,
) {
for object in &outline.objects {
- let [x0, y0, x1, y1] = object.rect;
- let rect = match moving {
- Some((id, rect)) if id == object.id => rect,
- _ => [
- x0 + origin[0],
- y0 + origin[1],
- x1 + origin[0],
- y1 + origin[1],
- ],
- };
- let picture = self.pictures.get(&object.id);
- if let Some(image) = picture.and_then(Picture::image) {
- primitives.push(Primitive::Image { image, rect });
+ self.append_object(object, origin, moving, paper, primitives);
+ }
+ }
+
+ /// One picture, file, drawing or placeholder laid out from `origin`; `moving` as for
+ /// `append_outline_objects`.
+ fn append_object<'a>(
+ &'a self,
+ object: &'a crate::outline::ObjectLayout,
+ origin: [f32; 2],
+ moving: Option<(onestore::ExGuid, [f32; 4])>,
+ paper: Paper,
+ primitives: &mut Vec>,
+ ) {
+ let [x0, y0, x1, y1] = object.rect;
+ let rect = match moving {
+ Some((id, rect)) if id == object.id => rect,
+ _ => [
+ x0 + origin[0],
+ y0 + origin[1],
+ x1 + origin[0],
+ y1 + origin[1],
+ ],
+ };
+ let picture = self.pictures.get(&object.id);
+ if let Some(image) = picture.and_then(Picture::image) {
+ primitives.push(Primitive::Image { image, rect });
+ }
+ match &object.kind {
+ crate::outline::ObjectKind::Ink(ink) => append_ink(ink, [rect[0], rect[1]], primitives),
+ crate::outline::ObjectKind::Unsupported(_) => {
+ append_placeholder(rect, paper, primitives)
}
- match &object.kind {
- crate::outline::ObjectKind::Ink(ink) => {
- append_ink(ink, [rect[0], rect[1]], primitives)
- }
- crate::outline::ObjectKind::Unsupported(_) => {
- append_placeholder(rect, paper, primitives)
- }
- crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => {
- append_placeholder(rect, paper, primitives)
- }
- crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}
+ crate::outline::ObjectKind::Picture if picture.is_none_or(Picture::failed) => {
+ append_placeholder(rect, paper, primitives)
}
- if let Some(label) = object.label() {
- primitives.push(Primitive::Text {
- text: &label.text,
- origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
- clip: None,
- ink: paper.ink,
- });
+ // A file stored without the icon OneNote rendered for it shows a blank page.
+ crate::outline::ObjectKind::File(_) if picture.is_none() => {
+ primitives.push(Primitive::Icon {
+ sources: &[FILE_ICON],
+ origin: [rect[0], rect[1]],
+ size: rect[2] - rect[0],
+ tint: [1.0; 4],
+ palette: draw::Palette::default(),
+ })
}
+ crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}
+ }
+ if let Some(label) = object.label() {
+ primitives.push(Primitive::Text {
+ text: &label.text,
+ origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
+ clip: None,
+ ink: paper.ink,
+ });
}
}
@@ -558,6 +618,7 @@ impl PageScene {
],
false,
)),
+ Content::File { .. } => Some((object.file()?.1, true)),
Content::Ink(ink) => Some((crate::editor::page::ink_bounds(ink)?, true)),
Content::ReadOnly(object) => Some((object.rect(), true)),
Content::Editable(_) => None,
@@ -652,6 +713,12 @@ impl PageScene {
return Some(SceneHit::Image(source.id));
}
}
+ Content::File { .. } => {
+ let (file, [x0, y0, x1, y1]) = object.file()?;
+ if (x0..=x1).contains(&point[0]) && (y0..=y1).contains(&point[1]) {
+ return Some(SceneHit::File(file.id));
+ }
+ }
Content::Outline { .. } | Content::Image(_) | Content::Ink(_) => {}
}
}
@@ -737,13 +804,43 @@ impl PageScene {
append_ink(ink, [offset[0] + x, offset[1] + y], primitives);
continue;
}
+ Content::File { source, layout } => {
+ // A dragged file draws where its column would land.
+ let [x, y] = match moving {
+ Some((id, [x0, y0, ..])) if id == source.id => [x0, y0],
+ _ => [source.layout.x, source.layout.y].map(|v| v.unwrap_or(0.0)),
+ };
+ self.append_object(
+ layout,
+ [x + offset[0], y + offset[1]],
+ None,
+ paper,
+ primitives,
+ );
+ continue;
+ }
};
let object_origin = [origin[0] + offset[0], origin[1] + offset[1]];
match content {
Content::Image(source) => {
- // The paper shows until the worker's raster lands.
let (image, [width, height]) = match self.backgrounds.get(&source.id) {
- Some(art) => (art.image(paper), art.size),
+ Some(art) => {
+ // OneNote's art is opaque, over white, and hides the rule lines.
+ primitives.push(Primitive::Rect {
+ rect: [
+ object_origin[0],
+ object_origin[1],
+ object_origin[0] + art.size[0],
+ object_origin[1] + art.size[1],
+ ],
+ color: if background::dark(paper) {
+ paper.color
+ } else {
+ Paper::WHITE.color
+ },
+ });
+ (art.image(paper), art.size)
+ }
None => {
let picture = self
.pictures
@@ -816,7 +913,10 @@ impl PageScene {
);
}
}
- Content::Editable(_) | Content::ReadOnly(_) | Content::Ink(_) => unreachable!(),
+ Content::Editable(_)
+ | Content::ReadOnly(_)
+ | Content::Ink(_)
+ | Content::File { .. } => unreachable!(),
}
}
Ok(())
diff --git a/crates/canvas/src/gpu/page/background.rs b/crates/canvas/src/gpu/page/background.rs
index 31c77ad6e93123ce1bcf8bd1ed9386f053b7fc5d..a6456df86cd5ace5be35b78c4eb0d7c65b29daac 100644
--- a/crates/canvas/src/gpu/page/background.rs
+++ b/crates/canvas/src/gpu/page/background.rs
@@ -402,6 +402,10 @@ const MARKER: &str = "Snowbound template";
/// device pixels per point.
const MAX_RASTER_BYTES: f32 = 16.0 * 1024.0 * 1024.0;
+/// Art an edit brings draws in its first frame in at most this many pixels, a few
+/// milliseconds' work, then sharpens.
+const INLINE_PIXELS: f32 = 1024.0 * 1024.0;
+
/// Template art standing in for a page's background picture.
pub(super) struct Background {
svg: &'static str,
@@ -409,14 +413,19 @@ pub(super) struct Background {
pub size: [f32; 2],
/// For light and for dark paper.
variants: [Variant; 2],
+ /// Whether the first raster is made on the frame's thread rather than awaited.
+ pub inline: bool,
}
+/// A raster's device pixels per point, and the dark paper it was recoloured for.
+type Made = (f32, Option<[f32; 4]>);
+
#[derive(Default)]
struct Variant {
- /// The raster painted and its device pixels per point.
- shown: Option<(RasterImage, f32)>,
- /// The one raster being made and its device pixels per point; replacing it abandons it.
- pending: Option<(Slot, f32)>,
+ /// The raster painted and what it was made for.
+ shown: Option<(RasterImage, Made)>,
+ /// The one raster being made and what for; replacing it abandons it.
+ pending: Option<(Slot, Made)>,
}
impl Background {
@@ -451,6 +460,7 @@ impl Background {
svg,
size,
variants: Default::default(),
+ inline: false,
})
}
@@ -469,38 +479,44 @@ impl Background {
let density = density(scale)
.min((MAX_RASTER_BYTES / (4.0 * width * height)).sqrt())
.min(4096.0 / width.max(height));
+ let made = (density, dark.then_some(paper.color));
+ let svg = self.svg;
+ let paper = dark.then_some(paper);
+ let raster = move |density: f32| {
+ let size = [width, height].map(|side| (side * density).round().max(1.0) as u32);
+ move || rasterize(svg, paper, size)
+ };
let variant = &mut self.variants[usize::from(dark)];
let landed = variant
.pending
.as_ref()
.and_then(|(slot, made)| Some((slot.lock().unwrap().take()?, *made)));
- if let Some((image, made)) = landed {
- if made == density || variant.shown.is_none() {
- variant.shown = Some((image, made));
+ if let Some((image, landed)) = landed {
+ if landed == made || variant.shown.is_none() {
+ variant.shown = Some((image, landed));
}
variant.pending = None;
}
+ if variant.shown.is_none() && std::mem::take(&mut self.inline) {
+ let quick = density.min((INLINE_PIXELS / (width * height)).sqrt());
+ variant.shown = Some((raster(quick)(), (quick, made.1)));
+ }
let shown = variant.shown.as_ref().map(|(_, made)| *made);
- if shown != Some(density)
- && variant.pending.as_ref().map(|(_, made)| *made) != Some(density)
- {
- let svg = self.svg;
- let paper = dark.then_some(paper);
- let size = [width, height].map(|side| (side * density).round().max(1.0) as u32);
+ if shown != Some(made) && variant.pending.as_ref().map(|(_, made)| *made) != Some(made) {
let start = Instant::now()
+ if shown.is_some() {
SETTLE
} else {
Duration::ZERO
};
- let slot = queue(start, waker, move || rasterize(svg, paper, size));
- variant.pending = Some((slot, density));
+ let slot = queue(start, waker, raster(density));
+ variant.pending = Some((slot, made));
}
- shown == Some(density)
+ shown == Some(made)
}
}
-fn dark(paper: Paper) -> bool {
+pub(super) fn dark(paper: Paper) -> bool {
let [lightness, ..] = draw::oklab(paper.color);
let [ink, ..] = draw::oklab(paper.ink);
ink > lightness
@@ -514,6 +530,19 @@ fn rasterize(svg: &str, dark: Option, size: [u32; 2]) -> RasterImage {
RasterImage::new(size, pixels).expect("Art is within the image budget")
}
+/// `svg` as a PNG `side` pixels square.
+pub(super) fn png(svg: &str, side: u32) -> Vec {
+ let mut encoded = Vec::new();
+ let mut encoder = png::Encoder::new(&mut encoded, side, side);
+ encoder.set_color(png::ColorType::Rgba);
+ encoder.set_depth(png::BitDepth::Eight);
+ encoder
+ .write_header()
+ .and_then(|mut writer| writer.write_image_data(&render(svg, [side; 2])))
+ .expect("A PNG encodes into memory");
+ encoded
+}
+
/// Straight-alpha RGBA rows of `svg` stretched to `width` by `height` pixels.
fn render(svg: &str, [width, height]: [u32; 2]) -> Vec {
let tree = parse(svg);
@@ -747,6 +776,7 @@ mod tests {
svg: TEMPLATES.iter().find(|t| t.name == "ivy").unwrap().svg,
size: [64.0, 48.0],
variants: Default::default(),
+ inline: false,
}
}
@@ -805,6 +835,13 @@ mod tests {
}
}
+ #[test]
+ fn every_template_s_art_is_recreated() {
+ for art in crate::template::TEMPLATES.iter().flat_map(|t| t.art) {
+ assert!(TEMPLATES.iter().any(|t| t.name == art.art), "{}", art.art);
+ }
+ }
+
#[test]
fn our_template_pictures_are_recognised_as_the_template() {
for (name, magic, size) in [
diff --git a/crates/canvas/src/interaction/accessibility.rs b/crates/canvas/src/interaction/accessibility.rs
index 985ce87b8ad0437c98feec39ce977577cc8136f1..c4b2d33ab987b057760be50f3af52345c5923c28 100644
--- a/crates/canvas/src/interaction/accessibility.rs
+++ b/crates/canvas/src/interaction/accessibility.rs
@@ -594,38 +594,15 @@ fn runs_of(shaped: &ParagraphLayout, breaks: bool) -> Result
.tags
.iter()
.map(|tag| {
- use crate::outline::TagIcon;
- let fallback = match tag.icon {
- TagIcon::CheckBox { .. } => "To do",
- TagIcon::Star => "Important",
- TagIcon::Question => "Question",
- TagIcon::Highlight => "Highlight",
- TagIcon::Contact => "Contact",
- TagIcon::Address => "Address",
- TagIcon::Phone => "Phone number",
- TagIcon::Music => "Music",
- TagIcon::Exclamation => "Critical",
- TagIcon::RedSquare => "Project A",
- TagIcon::YellowSquare => "Project B",
- TagIcon::BlueSquare => "Project C",
- TagIcon::Flag => "Outlook task",
- TagIcon::Idea => "Idea",
- TagIcon::Password => "Password",
- TagIcon::Movie => "Movie to see",
- TagIcon::Book => "Book to read",
- TagIcon::Web => "Web site to visit",
- TagIcon::Blog => "Remember for blog",
- TagIcon::Email => "Send in email",
- TagIcon::Other => "Tag",
- };
- let label = if tag.label.is_empty() {
- fallback
- } else {
- &tag.label
+ use crate::outline::{TagIcon, symbol_name};
+ let label = match tag.icon {
+ _ if !tag.label.is_empty() => &tag.label,
+ TagIcon::Symbol { shape, .. } => symbol_name(shape).unwrap_or("Tag"),
+ TagIcon::Task => "Outlook task",
};
let state = match tag.icon {
- TagIcon::CheckBox { checked: true, .. } => ", completed",
- TagIcon::CheckBox { checked: false, .. } => ", incomplete",
+ TagIcon::Symbol { checked: true, .. } => ", completed",
+ icon if icon.checkable() => ", incomplete",
_ => "",
};
format!(
@@ -1026,7 +1003,7 @@ pub(super) mod tests {
"Rehearsal, incomplete",
"Rehearsal, completed",
"Question",
- "Music, disabled"
+ "Music Note, disabled"
]
);
let text = state.focus().unwrap().document_range().text();
diff --git a/crates/canvas/src/interaction/mod.rs b/crates/canvas/src/interaction/mod.rs
index cd5c7b6c9629a60c9b377173e75c9280b3253083..3710fef7a6b230d0376a9a22f97a5ed71799f09d 100644
--- a/crates/canvas/src/interaction/mod.rs
+++ b/crates/canvas/src/interaction/mod.rs
@@ -60,6 +60,8 @@ pub enum Request {
Paste,
/// Open a link's address, as a click on a link does.
OpenLink(String),
+ /// Open a copy of a file with the system's application for it, as a double click does.
+ OpenAttachment(onestore::page::Attachment),
}
/// What a secondary press lands on, which its context menu offers commands for.
@@ -72,6 +74,8 @@ pub struct Context {
pub selected: bool,
/// The paragraph at the caret, which Copy Link to Paragraph names.
pub paragraph: Option,
+ /// The file the press selected, which Open and Save As take.
+ pub attachment: Option,
}
/// What an event did: `changed` means the page or selection changed (the host saves,
@@ -175,13 +179,15 @@ enum PointerFeedback<'a> {
pub enum ObjectFocus {
ReadOnly(usize),
Image(onestore::ExGuid),
+ /// A file in an outline's flow or on the page.
+ File(onestore::ExGuid),
}
impl ObjectFocus {
pub fn read_only(self) -> Option {
match self {
Self::ReadOnly(index) => Some(index),
- Self::Image(_) => None,
+ Self::Image(_) | Self::File(_) => None,
}
}
}
@@ -519,6 +525,7 @@ impl PageView {
let (origin, size) = self.editor.image_placement(id).unwrap();
image_rect(origin, size)
}
+ ObjectFocus::File(id) => self.editor.attachment_rect(id).unwrap(),
};
[
x0 + offset[0],
@@ -861,7 +868,7 @@ impl PageView {
{
Cursor::Pointer
}
- (None, Some(Hit::ReadOnly(_) | Hit::Check { .. })) => Cursor::Default,
+ (None, Some(Hit::ReadOnly(_) | Hit::Check { .. } | Hit::File(_))) => Cursor::Default,
_ => Cursor::Text,
}
}
@@ -998,6 +1005,24 @@ impl PageView {
.click_check(&mut self.engine, outline, paragraph)?;
return self.changed();
}
+ Some(Hit::File(id)) => {
+ self.set_object_focus(Some(ObjectFocus::File(id)));
+ self.drag = None;
+ if unit != SelectionUnit::Grapheme
+ && let Some(file) = self.editor.attachment(id)
+ {
+ return Ok(Response::request(Request::OpenAttachment(file.clone())));
+ }
+ // A file on the page drags as a picture does.
+ if !self.editor.image_in_outline(id) {
+ self.drag = Some(Drag::Image {
+ id,
+ handle: [0, 0],
+ press: point,
+ pending_press: Some(self.pointer),
+ });
+ }
+ }
Some(Hit::Image { id, handle }) => {
self.set_object_focus(Some(ObjectFocus::Image(id)));
self.drag = Some(Drag::Image {
@@ -1062,15 +1087,7 @@ impl PageView {
});
}
None => {
- // A fresh click places text 7 px above the pointer, on the grid.
- let position = [point[0], point[1] - 7.0 * self.pixel()];
- let position = if self.modifiers.option {
- position
- } else {
- snap_to_grid(position, self.editor.margin_origin())
- };
- self.editor
- .place_caret(&mut self.engine, position, DEFAULT_OUTLINE_WIDTH)?;
+ self.place_caret(point)?;
self.set_object_focus(None);
self.drag = Some(Drag::Text {
anchor: self.editor.selection(),
@@ -1081,6 +1098,19 @@ impl PageView {
self.changed()
}
+ /// A fresh click places text 7 px above the pointer, on the grid.
+ fn place_caret(&mut self, point: [f32; 2]) -> Result<()> {
+ let position = [point[0], point[1] - 7.0 * self.pixel()];
+ let position = if self.modifiers.option {
+ position
+ } else {
+ snap_to_grid(position, self.editor.margin_origin())
+ };
+ self.editor
+ .place_caret(&mut self.engine, position, DEFAULT_OUTLINE_WIDTH)?;
+ Ok(())
+ }
+
pub fn pointer_released(&mut self) -> Result {
if let Some(Drag::Space { axis, line }) = self.drag {
self.drag = None;
@@ -1261,11 +1291,22 @@ impl PageView {
}
/// A secondary press at the pointer: text there takes the caret unless it lies in the
- /// selection, as OneNote's context menu acts where it opens. `None` off text.
+ /// selection, and a file is selected, as OneNote's context menu acts where it opens.
+ /// `None` off text and files.
pub fn context(&mut self) -> Result