| author | |
| committer | |
| log | 4527a055161bfd2ca5f5beb5d38f2c6cceb675ad |
| tree | 11ccc72e94bad172eb0a8b691cf251144073bbe0 |
| parent | 9954b152f54c18a0150bbf7ef92fc56b4c23d2a8 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Five of the notebook's lyric and script pages opened with text boxes read-only because of their
tags. Font-colour tags (Action, Mood) now paint the paragraph, the newest colour tag winning;
Critical and the Project A/B/C squares have icons drawn from OneNote 2010 captures; and tags
follow OneNote's order and column: stored newest first but drawn oldest first, in one column
per outline sized by its most-tagged paragraph. "m: the train song" matches OneNote position
for position.
Assisted-by: claude-opus-5.510 files changed, 231 insertions(+), 26 deletions(-)
crates/canvas/assets/tags/blue-square.svg created+19| ... | @@ -0,0 +1,19 @@ | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop offset="0" stop-color="#52719c"/> | ||
| 5 | <stop offset="1" stop-color="#4a6994"/> | ||
| 6 | </linearGradient> | ||
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 8 | <stop offset="0" stop-color="#73a2e7"/> | ||
| 9 | <stop offset="1" stop-color="#7baeef"/> | ||
| 10 | </linearGradient> | ||
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 12 | <stop offset="0" stop-color="#6386bd"/> | ||
| 13 | <stop offset="1" stop-color="#5a7db5"/> | ||
| 14 | </linearGradient> | ||
| 15 | </defs> | ||
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | ||
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | ||
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | ||
| 19 | </svg> | ||
crates/canvas/assets/tags/exclamation.svg created+9| ... | @@ -0,0 +1,9 @@ | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop offset="0" stop-color="#ff7d52"/> | ||
| 5 | <stop offset="1" stop-color="#c4471f"/> | ||
| 6 | </linearGradient> | ||
| 7 | </defs> | ||
| 8 | <path d="M8 1.5C9.6 1.5 9.9 2.7 9.6 4.4L8.6 9.5H7.4L6.4 4.4C6.1 2.7 6.4 1.5 8 1.5ZM8 11C8.8 11 9.3 11.5 9.3 12.3C9.3 13.1 8.8 13.6 8 13.6C7.2 13.6 6.7 13.1 6.7 12.3C6.7 11.5 7.2 11 8 11Z" fill="url(#layer0)"/> | ||
| 9 | </svg> | ||
crates/canvas/assets/tags/red-square.svg created+19| ... | @@ -0,0 +1,19 @@ | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop offset="0" stop-color="#ce5963"/> | ||
| 5 | <stop offset="1" stop-color="#a5494a"/> | ||
| 6 | </linearGradient> | ||
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 8 | <stop offset="0" stop-color="#ff7173"/> | ||
| 9 | <stop offset="1" stop-color="#ff717b"/> | ||
| 10 | </linearGradient> | ||
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 12 | <stop offset="0" stop-color="#d66163"/> | ||
| 13 | <stop offset="1" stop-color="#ce5963"/> | ||
| 14 | </linearGradient> | ||
| 15 | </defs> | ||
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | ||
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | ||
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | ||
| 19 | </svg> | ||
crates/canvas/assets/tags/yellow-square.svg created+19| ... | @@ -0,0 +1,19 @@ | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop offset="0" stop-color="#ce9a52"/> | ||
| 5 | <stop offset="1" stop-color="#946d39"/> | ||
| 6 | </linearGradient> | ||
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 8 | <stop offset="0" stop-color="#fff37b"/> | ||
| 9 | <stop offset="1" stop-color="#ffeb7b"/> | ||
| 10 | </linearGradient> | ||
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | ||
| 12 | <stop offset="0" stop-color="#ffc763"/> | ||
| 13 | <stop offset="1" stop-color="#efb25a"/> | ||
| 14 | </linearGradient> | ||
| 15 | </defs> | ||
| 16 | <path d="M3 3H13V13H3Z" fill="url(#layer0)"/> | ||
| 17 | <path d="M3 3H12V12H3Z" fill="url(#layer1)"/> | ||
| 18 | <path d="M4 4H12V12H4Z" fill="url(#layer2)"/> | ||
| 19 | </svg> | ||
crates/canvas/src/gpu/page.rs+4-1| ... | @@ -497,7 +497,10 @@ impl PageScene { | ... | @@ -497,7 +497,10 @@ impl PageScene { |
| 497 | for tag in &paragraph.tags { | 497 | for tag in &paragraph.tags { |
| 498 | primitives.push(Primitive::Tag { | 498 | primitives.push(Primitive::Tag { |
| 499 | tag, | 499 | tag, |
| 500 | origin: [object_origin[0], object_origin[1] + paragraph.origin[1]], | 500 | origin: [ |
| 501 | object_origin[0] + outline.tag_column_offset(), | ||
| 502 | object_origin[1] + paragraph.origin[1], | ||
| 503 | ], | ||
| 501 | }); | 504 | }); |
| 502 | } | 505 | } |
| 503 | } | 506 | } |
crates/canvas/src/gpu/tags.rs+8| ... | @@ -12,6 +12,10 @@ pub(crate) fn rasterize(icon: TagIcon, size: f32, phase: [u8; 2]) -> Image { | ... | @@ -12,6 +12,10 @@ pub(crate) fn rasterize(icon: TagIcon, size: f32, phase: [u8; 2]) -> Image { |
| 12 | ), | 12 | ), |
| 13 | TagIcon::Question => (include_str!("../../assets/tags/question.svg"), None), | 13 | TagIcon::Question => (include_str!("../../assets/tags/question.svg"), None), |
| 14 | TagIcon::Music => (include_str!("../../assets/tags/music.svg"), None), | 14 | TagIcon::Music => (include_str!("../../assets/tags/music.svg"), None), |
| 15 | TagIcon::Exclamation => (include_str!("../../assets/tags/exclamation.svg"), None), | ||
| 16 | TagIcon::RedSquare => (include_str!("../../assets/tags/red-square.svg"), None), | ||
| 17 | TagIcon::YellowSquare => (include_str!("../../assets/tags/yellow-square.svg"), None), | ||
| 18 | TagIcon::BlueSquare => (include_str!("../../assets/tags/blue-square.svg"), None), | ||
| 15 | }; | 19 | }; |
| 16 | let width = (size + 1.0).ceil() as u32; | 20 | let width = (size + 1.0).ceil() as u32; |
| 17 | let mut pixels = vec![[0.0_f32; 4]; (width * width) as usize]; | 21 | let mut pixels = vec![[0.0_f32; 4]; (width * width) as usize]; |
| ... | @@ -107,6 +111,10 @@ mod tests { | ... | @@ -107,6 +111,10 @@ mod tests { |
| 107 | TagIcon::CheckBox { checked: true }, | 111 | TagIcon::CheckBox { checked: true }, |
| 108 | TagIcon::Question, | 112 | TagIcon::Question, |
| 109 | TagIcon::Music, | 113 | TagIcon::Music, |
| 114 | TagIcon::Exclamation, | ||
| 115 | TagIcon::RedSquare, | ||
| 116 | TagIcon::YellowSquare, | ||
| 117 | TagIcon::BlueSquare, | ||
| 110 | ] { | 118 | ] { |
| 111 | let image = rasterize(icon, size, phase); | 119 | let image = rasterize(icon, size, phase); |
| 112 | assert_eq!( | 120 | assert_eq!( |
crates/canvas/src/outline.rs+142-21| ... | @@ -74,6 +74,10 @@ pub enum TagIcon { | ... | @@ -74,6 +74,10 @@ pub enum TagIcon { |
| 74 | CheckBox { checked: bool }, | 74 | CheckBox { checked: bool }, |
| 75 | Question, | 75 | Question, |
| 76 | Music, | 76 | Music, |
| 77 | Exclamation, | ||
| 78 | RedSquare, | ||
| 79 | YellowSquare, | ||
| 80 | BlueSquare, | ||
| 77 | } | 81 | } |
| 78 | 82 | ||
| 79 | #[derive(Clone, Debug, PartialEq)] | 83 | #[derive(Clone, Debug, PartialEq)] |
| ... | @@ -155,7 +159,50 @@ impl ParagraphLayout { | ... | @@ -155,7 +159,50 @@ impl ParagraphLayout { |
| 155 | if !spacing.is_finite() || spacing < 0.0 { | 159 | if !spacing.is_finite() || spacing < 0.0 { |
| 156 | return Err(LayoutError::InvalidSpacing); | 160 | return Err(LayoutError::InvalidSpacing); |
| 157 | } | 161 | } |
| 158 | let mut text = engine.layout(projection.text(), width - indent)?; | 162 | // Stored newest first; OneNote lists and paints tags oldest first. |
| 163 | let mut tag_definitions = paragraph | ||
| 164 | .tags | ||
| 165 | .iter() | ||
| 166 | .chain(&source.tags) | ||
| 167 | .map( | ||
| 168 | |tag| match tag.definition.as_ref().and_then(|id| definitions.get(id)) { | ||
| 169 | Some(Definition { | ||
| 170 | kind: | ||
| 171 | Kind::TagDefinition { | ||
| 172 | shape, | ||
| 173 | label, | ||
| 174 | color, | ||
| 175 | highlight, | ||
| 176 | .. | ||
| 177 | }, | ||
| 178 | .. | ||
| 179 | }) if tag.status & 4 == 0 => Ok((tag, *shape, label, *color, *highlight)), | ||
| 180 | _ => Err(LayoutError::UnsupportedContent), | ||
| 181 | }, | ||
| 182 | ) | ||
| 183 | .collect::<Result<Vec<_>, _>>()?; | ||
| 184 | tag_definitions.reverse(); | ||
| 185 | // The newest tag that sets a colour wins. | ||
| 186 | let color = tag_definitions.iter().rev().find_map(|tag| tag.3); | ||
| 187 | let highlight = tag_definitions.iter().rev().find_map(|tag| tag.4); | ||
| 188 | let mut text = if color.is_none() && highlight.is_none() { | ||
| 189 | engine.layout(projection.text(), width - indent)? | ||
| 190 | } else { | ||
| 191 | let visible = projection.text(); | ||
| 192 | let mut start = 0; | ||
| 193 | let runs = visible.spans().iter().map(|span| { | ||
| 194 | let mut format = span.format.clone(); | ||
| 195 | format.color = color.or(format.color); | ||
| 196 | format.highlight = highlight.or(format.highlight); | ||
| 197 | let run = (visible.text()[start..span.end].to_owned(), format); | ||
| 198 | start = span.end; | ||
| 199 | run | ||
| 200 | }); | ||
| 201 | engine.layout( | ||
| 202 | &Paragraph::from_runs(runs.collect::<Vec<_>>()), | ||
| 203 | width - indent, | ||
| 204 | )? | ||
| 205 | }; | ||
| 159 | let mut markers = Vec::new(); | 206 | let mut markers = Vec::new(); |
| 160 | let mut marker_x = indent; | 207 | let mut marker_x = indent; |
| 161 | for id in paragraph.lists.iter().rev() { | 208 | for id in paragraph.lists.iter().rev() { |
| ... | @@ -194,37 +241,26 @@ impl ParagraphLayout { | ... | @@ -194,37 +241,26 @@ impl ParagraphLayout { |
| 194 | } | 241 | } |
| 195 | text.minimum_line_height(format.line_spacing.unwrap_or(0.0))?; | 242 | text.minimum_line_height(format.line_spacing.unwrap_or(0.0))?; |
| 196 | let mut tags = Vec::new(); | 243 | let mut tags = Vec::new(); |
| 197 | for tag in paragraph.tags.iter().chain(&source.tags) { | 244 | for tag in &tag_definitions { |
| 198 | let Some(Definition { | 245 | let (tag, shape, label) = (tag.0, tag.1, tag.2); |
| 199 | kind: | ||
| 200 | Kind::TagDefinition { | ||
| 201 | shape, | ||
| 202 | label, | ||
| 203 | color, | ||
| 204 | highlight, | ||
| 205 | .. | ||
| 206 | }, | ||
| 207 | .. | ||
| 208 | }) = tag.definition.as_ref().and_then(|id| definitions.get(id)) | ||
| 209 | else { | ||
| 210 | return Err(LayoutError::UnsupportedContent); | ||
| 211 | }; | ||
| 212 | if color.is_some() || highlight.is_some() || tag.status & 4 != 0 { | ||
| 213 | return Err(LayoutError::UnsupportedContent); | ||
| 214 | } | ||
| 215 | let icon = match shape { | 246 | let icon = match shape { |
| 216 | Some(0) => continue, | 247 | Some(0) => continue, |
| 217 | Some(3) => TagIcon::CheckBox { | 248 | Some(3) => TagIcon::CheckBox { |
| 218 | checked: tag.status & 1 != 0, | 249 | checked: tag.status & 1 != 0, |
| 219 | }, | 250 | }, |
| 220 | Some(15) => TagIcon::Question, | 251 | Some(15) => TagIcon::Question, |
| 252 | Some(17) => TagIcon::Exclamation, | ||
| 253 | Some(100) => TagIcon::RedSquare, | ||
| 254 | Some(101) => TagIcon::YellowSquare, | ||
| 255 | Some(102) => TagIcon::BlueSquare, | ||
| 221 | Some(121) => TagIcon::Music, | 256 | Some(121) => TagIcon::Music, |
| 222 | _ => return Err(LayoutError::UnsupportedContent), | 257 | _ => return Err(LayoutError::UnsupportedContent), |
| 223 | }; | 258 | }; |
| 224 | marker_x -= 20.25; | 259 | // Later tags follow the first to its right, toward the text. |
| 260 | let x = marker_x - 20.25 + 12.0 * tags.len() as f32; | ||
| 225 | tags.push(ParagraphTag { | 261 | tags.push(ParagraphTag { |
| 226 | icon, | 262 | icon, |
| 227 | origin: [marker_x, 0.0], | 263 | origin: [x, 0.0], |
| 228 | label: label.clone().unwrap_or_default(), | 264 | label: label.clone().unwrap_or_default(), |
| 229 | disabled: tag.status & 2 != 0, | 265 | disabled: tag.status & 2 != 0, |
| 230 | }); | 266 | }); |
| ... | @@ -292,6 +328,18 @@ pub(crate) fn arrange<'a>( | ... | @@ -292,6 +328,18 @@ pub(crate) fn arrange<'a>( |
| 292 | } | 328 | } |
| 293 | 329 | ||
| 294 | impl OutlineLayout { | 330 | impl OutlineLayout { |
| 331 | /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each | ||
| 332 | /// paragraph's tags start at the column's left edge. Tag origins assume a one-tag column. | ||
| 333 | pub fn tag_column_offset(&self) -> f32 { | ||
| 334 | let widest = self | ||
| 335 | .paragraphs | ||
| 336 | .iter() | ||
| 337 | .map(|p| p.tags.len()) | ||
| 338 | .max() | ||
| 339 | .unwrap_or(0); | ||
| 340 | -12.0 * widest.saturating_sub(1) as f32 | ||
| 341 | } | ||
| 342 | |||
| 295 | /// Innermost table cell containing a visible paragraph index. | 343 | /// Innermost table cell containing a visible paragraph index. |
| 296 | pub fn paragraph_cell(&self, index: usize) -> Option<&CellLayout> { | 344 | pub fn paragraph_cell(&self, index: usize) -> Option<&CellLayout> { |
| 297 | self.tables.iter().rev().find_map(|table| { | 345 | self.tables.iter().rev().find_map(|table| { |
| ... | @@ -932,6 +980,10 @@ mod tests { | ... | @@ -932,6 +980,10 @@ mod tests { |
| 932 | for (shape, expected) in [ | 980 | for (shape, expected) in [ |
| 933 | (3, TagIcon::CheckBox { checked: true }), | 981 | (3, TagIcon::CheckBox { checked: true }), |
| 934 | (15, TagIcon::Question), | 982 | (15, TagIcon::Question), |
| 983 | (17, TagIcon::Exclamation), | ||
| 984 | (100, TagIcon::RedSquare), | ||
| 985 | (101, TagIcon::YellowSquare), | ||
| 986 | (102, TagIcon::BlueSquare), | ||
| 935 | (121, TagIcon::Music), | 987 | (121, TagIcon::Music), |
| 936 | ] { | 988 | ] { |
| 937 | definitions.insert( | 989 | definitions.insert( |
| ... | @@ -1000,6 +1052,75 @@ mod tests { | ... | @@ -1000,6 +1052,75 @@ mod tests { |
| 1000 | )); | 1052 | )); |
| 1001 | } | 1053 | } |
| 1002 | 1054 | ||
| 1055 | #[test] | ||
| 1056 | fn tags_paint_oldest_first_and_the_newest_colour_wins() { | ||
| 1057 | use onestore::document::Tag; | ||
| 1058 | let mut definitions = BTreeMap::new(); | ||
| 1059 | let mut tag = |n, shape, color| { | ||
| 1060 | let id = ExGuid { | ||
| 1061 | n, | ||
| 1062 | ..ExGuid::default() | ||
| 1063 | }; | ||
| 1064 | definitions.insert( | ||
| 1065 | id, | ||
| 1066 | Definition { | ||
| 1067 | kind: Kind::TagDefinition { | ||
| 1068 | shape: Some(shape), | ||
| 1069 | label: None, | ||
| 1070 | action_type: None, | ||
| 1071 | color, | ||
| 1072 | highlight: None, | ||
| 1073 | }, | ||
| 1074 | format: Format::default(), | ||
| 1075 | }, | ||
| 1076 | ); | ||
| 1077 | Tag { | ||
| 1078 | definition: Some(id), | ||
| 1079 | status: 1, | ||
| 1080 | action_type: None, | ||
| 1081 | created: None, | ||
| 1082 | completed: None, | ||
| 1083 | start: None, | ||
| 1084 | due: None, | ||
| 1085 | task_id: None, | ||
| 1086 | extra_set: 0, | ||
| 1087 | } | ||
| 1088 | }; | ||
| 1089 | let [action, mood, project, question] = [ | ||
| 1090 | tag(1, 0, Some(0x0080_0080)), | ||
| 1091 | tag(2, 0, Some(0x0080_8000)), | ||
| 1092 | tag(3, 100, None), | ||
| 1093 | tag(4, 15, None), | ||
| 1094 | ]; | ||
| 1095 | let mut node = paragraph(1, "Lyric", 1, None); | ||
| 1096 | // As stored: newest first. | ||
| 1097 | node.text_mut().unwrap().tags = vec![question, project, mood, action]; | ||
| 1098 | let mut engine = TextEngine::default(); | ||
| 1099 | let shaped = | ||
| 1100 | ParagraphLayout::shape(&mut engine, &node, 200.0, &[0.0, 0.0], &definitions).unwrap(); | ||
| 1101 | assert_eq!( | ||
| 1102 | shaped | ||
| 1103 | .tags | ||
| 1104 | .iter() | ||
| 1105 | .map(|tag| (tag.icon, tag.origin[0])) | ||
| 1106 | .collect::<Vec<_>>(), | ||
| 1107 | [(TagIcon::RedSquare, -20.25), (TagIcon::Question, -8.25)] | ||
| 1108 | ); | ||
| 1109 | let colors: Vec<_> = shaped | ||
| 1110 | .text | ||
| 1111 | .lines() | ||
| 1112 | .flat_map(|(line, _)| line.items().collect::<Vec<_>>()) | ||
| 1113 | .filter_map(|item| match item { | ||
| 1114 | parley::PositionedLayoutItem::GlyphRun(run) => Some(run.style().brush.color), | ||
| 1115 | _ => None, | ||
| 1116 | }) | ||
| 1117 | .collect(); | ||
| 1118 | assert_eq!(colors, [0x0080_8000]); | ||
| 1119 | assert_eq!(shaped.projection.text().spans()[0].format.color, None); | ||
| 1120 | let outline = OutlineLayout::new(vec![shaped], 200.0, false).unwrap(); | ||
| 1121 | assert_eq!(outline.tag_column_offset(), -12.0); | ||
| 1122 | } | ||
| 1123 | |||
| 1003 | #[test] | 1124 | #[test] |
| 1004 | fn title_uses_a_default_wrap_limit_and_keeps_date_after_wrapped_title() { | 1125 | fn title_uses_a_default_wrap_limit_and_keeps_date_after_wrapped_title() { |
| 1005 | let mut title = Title { | 1126 | let mut title = Title { |
crates/snowbound/src/accessibility.rs+4| ... | @@ -366,6 +366,10 @@ impl Accessibility { | ... | @@ -366,6 +366,10 @@ impl Accessibility { |
| 366 | TagIcon::CheckBox { .. } => "To do", | 366 | TagIcon::CheckBox { .. } => "To do", |
| 367 | TagIcon::Question => "Question", | 367 | TagIcon::Question => "Question", |
| 368 | TagIcon::Music => "Music", | 368 | TagIcon::Music => "Music", |
| 369 | TagIcon::Exclamation => "Critical", | ||
| 370 | TagIcon::RedSquare => "Project A", | ||
| 371 | TagIcon::YellowSquare => "Project B", | ||
| 372 | TagIcon::BlueSquare => "Project C", | ||
| 369 | }; | 373 | }; |
| 370 | let label = if tag.label.is_empty() { | 374 | let label = if tag.label.is_empty() { |
| 371 | fallback | 375 | fallback |
crates/snowbound/src/main.rs+5-2| ... | @@ -1753,7 +1753,7 @@ fn page_hit_test( | ... | @@ -1753,7 +1753,7 @@ fn page_hit_test( |
| 1753 | || outline.layouts().any(|(_, paragraph)| { | 1753 | || outline.layouts().any(|(_, paragraph)| { |
| 1754 | paragraph.tags.iter().any(|tag| { | 1754 | paragraph.tags.iter().any(|tag| { |
| 1755 | let origin = outline.origin(); | 1755 | let origin = outline.origin(); |
| 1756 | let x = origin[0] + tag.origin[0]; | 1756 | let x = origin[0] + outline.shaped().tag_column_offset() + tag.origin[0]; |
| 1757 | let y = origin[1] + paragraph.origin[1] + tag.origin[1]; | 1757 | let y = origin[1] + paragraph.origin[1] + tag.origin[1]; |
| 1758 | let size = canvas::outline::ParagraphTag::SIZE; | 1758 | let size = canvas::outline::ParagraphTag::SIZE; |
| 1759 | (x..=x + size).contains(&local[0]) && (y..=y + size).contains(&local[1]) | 1759 | (x..=x + size).contains(&local[0]) && (y..=y + size).contains(&local[1]) |
| ... | @@ -2211,7 +2211,10 @@ fn append_outline<'a>( | ... | @@ -2211,7 +2211,10 @@ fn append_outline<'a>( |
| 2211 | for tag in &paragraph.tags { | 2211 | for tag in &paragraph.tags { |
| 2212 | primitives.push(Primitive::Tag { | 2212 | primitives.push(Primitive::Tag { |
| 2213 | tag, | 2213 | tag, |
| 2214 | origin: [x, y + paragraph_origin[1]], | 2214 | origin: [ |
| 2215 | x + outline.shaped().tag_column_offset(), | ||
| 2216 | y + paragraph_origin[1], | ||
| 2217 | ], | ||
| 2215 | }); | 2218 | }); |
| 2216 | } | 2219 | } |
| 2217 | } | 2220 | } |
tools/canvas/README.md+2-2| ... | @@ -45,9 +45,9 @@ Image textures have a separate 64 MiB / 256-entry cache. Offscreen images do not | ... | @@ -45,9 +45,9 @@ Image textures have a separate 64 MiB / 256-entry cache. Offscreen images do not |
| 45 | 45 | ||
| 46 | ## Note tags | 46 | ## Note tags |
| 47 | 47 | ||
| 48 | Paragraph layout retains blue checkbox (shape 3), question mark (15) and musical note (121) tags from paragraph and rich-text nodes. Their 12 pt tiles precede the list marker or text origin with a 20.25 pt gutter; they do not change text advances, wrapping or line height. These dimensions cover the three reference pages' 17 tags. Hand-authored vector paths use Swash's existing rasterizer and the text atlas, including its subpixel phases, offscreen culling and eviction. No Office bitmap assets or tag fonts are bundled. | 48 | Paragraph layout retains checkbox (shape 3), question mark (15), exclamation (17), red, yellow and blue square (100–102) and musical note (121) tags from paragraph and rich-text nodes. Tags are stored newest first and drawn oldest first: an outline has one tag column, as wide as its most-tagged paragraph needs (12 pt per tag), whose left edge sits 20.25 pt plus 12 pt per extra tag before the list marker or text origin, and each paragraph's tags start at that edge. Tags do not change text advances, wrapping or line height. A tag's font colour or highlight paints the whole paragraph, the newest such tag winning; shape 0 draws no icon. Positions, order and colours match OneNote 2010 on the notebook's lyric pages. Hand-authored vector paths use Swash's existing rasterizer and the text atlas, including its subpixel phases, offscreen culling and eviction. No Office bitmap assets or tag fonts are bundled. |
| 49 | 49 | ||
| 50 | Checkboxes display completion; noncheckable tags ignore the completion bit required by the file format. Disabled tags dim. Accessibility descriptions expose source labels and checkbox/disabled state without changing selectable text. Tag state is read-only; text edits and undo retain it. Clicking a tag gutter focuses its outline. Unsupported icon shapes, task tags and tag color/highlight overrides return `UnsupportedContent` during layout. | 50 | Checkboxes display completion; noncheckable tags ignore the completion bit required by the file format. Disabled tags dim. Accessibility descriptions expose source labels and checkbox/disabled state without changing selectable text. Tag state is read-only; text edits and undo retain it. Clicking a tag gutter focuses its outline. Unsupported icon shapes and task tags return `UnsupportedContent` during layout. |
| 51 | 51 | ||
| 52 | ## Optional font substitution | 52 | ## Optional font substitution |
| 53 | 53 |