@@ -617,6 +617,163 @@ fn test_image_row_occupation_exact_fit() {
617617 ) ;
618618}
619619
620+ #[ test]
621+ fn test_subcell_offset_forwarded_and_clamped ( ) {
622+ let event_listener = TestEventListener ;
623+ let window_id = unsafe { WindowId :: dummy ( ) } ;
624+
625+ let mut term: Crosswords < TestEventListener > = Crosswords :: new (
626+ crate :: crosswords:: CrosswordsSize :: new ( 80 , 24 ) ,
627+ crate :: ansi:: CursorShape :: Block ,
628+ event_listener,
629+ window_id,
630+ 0 ,
631+ 10_000 ,
632+ ) ;
633+
634+ term. graphics . cell_width = 10.0 ;
635+ term. graphics . cell_height = 20.0 ;
636+
637+ let pixels = vec ! [ 255u8 ; 40 * 40 * 4 ] ;
638+ let graphic = GraphicData {
639+ id : GraphicId :: new ( 1 ) ,
640+ width : 40 ,
641+ height : 40 ,
642+ color_type : ColorType :: Rgba ,
643+ pixels,
644+ is_opaque : true ,
645+ resize : None ,
646+ display_width : None ,
647+ display_height : None ,
648+ transmit_time : std:: time:: Instant :: now ( ) ,
649+ } ;
650+ term. store_graphic ( graphic) ;
651+
652+ // In-range `X=`/`Y=` flows through to the stored placement.
653+ let placement = kitty_graphics_protocol:: PlacementRequest {
654+ image_id : 1 ,
655+ placement_id : 7 ,
656+ x : 0 ,
657+ y : 0 ,
658+ width : 0 ,
659+ height : 0 ,
660+ columns : 0 ,
661+ rows : 0 ,
662+ z_index : 0 ,
663+ virtual_placement : false ,
664+ unicode_placeholder : 0 ,
665+ cursor_movement : 1 ,
666+ cell_x_offset : 7 ,
667+ cell_y_offset : 9 ,
668+ } ;
669+ term. place_graphic ( placement) ;
670+
671+ let stored = term
672+ . graphics
673+ . kitty_placements
674+ . get ( & ( 1 , 7 ) )
675+ . expect ( "placement stored" ) ;
676+ assert_eq ! ( stored. cell_x_offset, 7 ) ;
677+ assert_eq ! ( stored. cell_y_offset, 9 ) ;
678+
679+ // Per kitty spec the offset must be smaller than the cell size;
680+ // out-of-range values are clamped to the cell box.
681+ let placement = kitty_graphics_protocol:: PlacementRequest {
682+ image_id : 1 ,
683+ placement_id : 8 ,
684+ x : 0 ,
685+ y : 0 ,
686+ width : 0 ,
687+ height : 0 ,
688+ columns : 0 ,
689+ rows : 0 ,
690+ z_index : 0 ,
691+ virtual_placement : false ,
692+ unicode_placeholder : 0 ,
693+ cursor_movement : 1 ,
694+ cell_x_offset : 999 ,
695+ cell_y_offset : 999 ,
696+ } ;
697+ term. place_graphic ( placement) ;
698+
699+ let stored = term
700+ . graphics
701+ . kitty_placements
702+ . get ( & ( 1 , 8 ) )
703+ . expect ( "placement stored" ) ;
704+ assert_eq ! ( stored. cell_x_offset, 9 , "clamped to cell_width - 1" ) ;
705+ assert_eq ! ( stored. cell_y_offset, 19 , "clamped to cell_height - 1" ) ;
706+ }
707+
708+ #[ test]
709+ fn test_subcell_offset_extends_row_occupation ( ) {
710+ let event_listener = TestEventListener ;
711+ let window_id = unsafe { WindowId :: dummy ( ) } ;
712+
713+ let mut term: Crosswords < TestEventListener > = Crosswords :: new (
714+ crate :: crosswords:: CrosswordsSize :: new ( 80 , 24 ) ,
715+ crate :: ansi:: CursorShape :: Block ,
716+ event_listener,
717+ window_id,
718+ 0 ,
719+ 10_000 ,
720+ ) ;
721+
722+ term. graphics . cell_width = 10.0 ;
723+ term. graphics . cell_height = 20.0 ;
724+
725+ // 40px tall image on 20px cells: exactly 2 rows without an offset.
726+ let pixels = vec ! [ 255u8 ; 40 * 40 * 4 ] ;
727+ let graphic = GraphicData {
728+ id : GraphicId :: new ( 1 ) ,
729+ width : 40 ,
730+ height : 40 ,
731+ color_type : ColorType :: Rgba ,
732+ pixels,
733+ is_opaque : true ,
734+ resize : None ,
735+ display_width : None ,
736+ display_height : None ,
737+ transmit_time : std:: time:: Instant :: now ( ) ,
738+ } ;
739+ term. store_graphic ( graphic) ;
740+
741+ // `Y=15` shifts the image down within its first cell, so it spills
742+ // into a third row: ceil((40 + 15) / 20) = 3. Cursor movement and
743+ // occupation must cover that extra row.
744+ let placement = kitty_graphics_protocol:: PlacementRequest {
745+ image_id : 1 ,
746+ placement_id : 7 ,
747+ x : 0 ,
748+ y : 0 ,
749+ width : 0 ,
750+ height : 0 ,
751+ columns : 0 ,
752+ rows : 0 ,
753+ z_index : 0 ,
754+ virtual_placement : false ,
755+ unicode_placeholder : 0 ,
756+ cursor_movement : 0 ,
757+ cell_x_offset : 0 ,
758+ cell_y_offset : 15 ,
759+ } ;
760+ term. place_graphic ( placement) ;
761+
762+ let stored = term
763+ . graphics
764+ . kitty_placements
765+ . get ( & ( 1 , 7 ) )
766+ . expect ( "placement stored" ) ;
767+ assert_eq ! ( stored. rows, 3 , "Y offset spills the image into a 3rd row" ) ;
768+ assert_eq ! ( stored. columns, 4 , "no X offset: 40px / 10px = 4 columns" ) ;
769+
770+ // C=0: cursor lands on the last row of the image (row index rows - 1).
771+ assert_eq ! (
772+ term. grid. cursor. pos. row. 0 , 2 ,
773+ "cursor advances to the extra row created by the Y offset"
774+ ) ;
775+ }
776+
620777#[ test]
621778fn test_image_row_occupation_single_row ( ) {
622779 let event_listener = TestEventListener ;
0 commit comments