1- use core_types:: ATTR_TRANSFORM ;
21use core_types:: table:: { Table , TableRow } ;
2+ use core_types:: { ATTR_EDITOR_CLICK_TARGET , ATTR_TRANSFORM } ;
33use glam:: { DAffine2 , DVec2 } ;
44use parley:: GlyphRun ;
55use skrifa:: GlyphId ;
@@ -15,6 +15,8 @@ pub struct PathBuilder {
1515 origin : DVec2 ,
1616 glyph_subpaths : Vec < Subpath < PointId > > ,
1717 pub vector_table : Table < Vector > ,
18+ /// Per-glyph bbox rectangles collected in single-row mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
19+ merged_click_target_subpaths : Vec < Subpath < PointId > > ,
1820 scale : f64 ,
1921 id : PointId ,
2022}
@@ -25,6 +27,7 @@ impl PathBuilder {
2527 current_subpath : Subpath :: new ( Vec :: new ( ) , false ) ,
2628 glyph_subpaths : Vec :: new ( ) ,
2729 vector_table : if per_glyph_items { Table :: new ( ) } else { Table :: new_from_element ( Vector :: default ( ) ) } ,
30+ merged_click_target_subpaths : Vec :: new ( ) ,
2831 scale,
2932 id : PointId :: ZERO ,
3033 origin : DVec2 :: default ( ) ,
@@ -51,14 +54,24 @@ impl PathBuilder {
5154 glyph_subpath. apply_transform ( skew) ;
5255 }
5356
57+ // Bounding-box rectangle for click-targeting the glyph's full bounds (not just the letterform)
58+ let glyph_bbox_rectangle = subpaths_bounding_box ( & self . glyph_subpaths ) . map ( |[ min, max] | Subpath :: new_rectangle ( min, max) ) ;
59+
5460 if per_glyph_items {
55- self . vector_table
56- . push ( TableRow :: new_from_element ( Vector :: from_subpaths ( core:: mem:: take ( & mut self . glyph_subpaths ) , false ) ) . with_attribute ( ATTR_TRANSFORM , DAffine2 :: from_translation ( glyph_offset) ) ) ;
61+ let row = TableRow :: new_from_element ( Vector :: from_subpaths ( core:: mem:: take ( & mut self . glyph_subpaths ) , false ) ) . with_attribute ( ATTR_TRANSFORM , DAffine2 :: from_translation ( glyph_offset) ) ;
62+ let row = match glyph_bbox_rectangle {
63+ Some ( rect) => row. with_attribute ( ATTR_EDITOR_CLICK_TARGET , Vector :: from_subpaths ( [ rect] , false ) ) ,
64+ None => row,
65+ } ;
66+ self . vector_table . push ( row) ;
5767 } else {
5868 for subpath in self . glyph_subpaths . drain ( ..) {
5969 // Unwrapping here is ok because `self.vector_table` is initialized with a single `Table<Vector>` item
6070 self . vector_table . element_mut ( 0 ) . unwrap ( ) . append_subpath ( subpath, false ) ;
6171 }
72+ if let Some ( rect) = glyph_bbox_rectangle {
73+ self . merged_click_target_subpaths . push ( rect) ;
74+ }
6275 }
6376 }
6477
@@ -120,10 +133,24 @@ impl PathBuilder {
120133 if self . vector_table . is_empty ( ) {
121134 self . vector_table = Table :: new_from_element ( Vector :: default ( ) ) ;
122135 }
136+
137+ // With "Separate Glyph Elements" inactive, combine the accumulated per-glyph AABBs as one override `Vector`
138+ if !self . merged_click_target_subpaths . is_empty ( ) {
139+ self . vector_table
140+ . set_attribute ( ATTR_EDITOR_CLICK_TARGET , 0 , Vector :: from_subpaths ( self . merged_click_target_subpaths , false ) ) ;
141+ }
142+
123143 self . vector_table
124144 }
125145}
126146
147+ fn subpaths_bounding_box ( subpaths : & [ Subpath < PointId > ] ) -> Option < [ DVec2 ; 2 ] > {
148+ subpaths
149+ . iter ( )
150+ . filter_map ( |subpath| subpath. bounding_box ( ) )
151+ . reduce ( |[ a_min, a_max] , [ b_min, b_max] | [ a_min. min ( b_min) , a_max. max ( b_max) ] )
152+ }
153+
127154impl OutlinePen for PathBuilder {
128155 fn move_to ( & mut self , x : f32 , y : f32 ) {
129156 if !self . current_subpath . is_empty ( ) {
0 commit comments