@@ -33,7 +33,7 @@ use winit::dpi::{PhysicalPosition, PhysicalSize};
3333use winit:: event:: { ElementState , MouseButton , MouseScrollDelta , WindowEvent } ;
3434use winit:: event_loop:: { ActiveEventLoop , EventLoop , EventLoopProxy } ;
3535use winit:: keyboard:: { Key , KeyCode , ModifiersState , NamedKey , PhysicalKey } ;
36- use winit:: window:: { Theme , Window , WindowId } ;
36+ use winit:: window:: { CursorIcon , Theme , Window , WindowId } ;
3737
3838use winit:: event_loop:: ControlFlow ;
3939
@@ -322,6 +322,9 @@ struct App {
322322 modifiers : ModifiersState ,
323323 mouse_pos : ( f32 , f32 ) ,
324324 mouse_down : bool ,
325+ /// The OS cursor icon currently set on the window, so hover updates only call
326+ /// `set_cursor` on a real change (Ctrl-hover over a link → pointer, else default).
327+ cursor_icon : CursorIcon ,
325328 /// Click-count tracking for double/triple-click select (winit doesn't provide it):
326329 /// the last press's time + position, and the running count (cycles 1→2→3).
327330 last_click : Option < ( std:: time:: Instant , ( f32 , f32 ) ) > ,
@@ -415,6 +418,7 @@ impl App {
415418 modifiers : ModifiersState :: empty ( ) ,
416419 mouse_pos : ( 0.0 , 0.0 ) ,
417420 mouse_down : false ,
421+ cursor_icon : CursorIcon :: Default ,
418422 last_click : None ,
419423 click_count : 0 ,
420424 drag_scroll_dy : 0.0 ,
@@ -1094,6 +1098,34 @@ fn ac_row_at(rects: &[ScreenRect], pos: (f32, f32)) -> Option<usize> {
10941098 rects. iter ( ) . position ( |r| rect_contains ( r, pos) )
10951099}
10961100
1101+ /// Point the OS cursor at whatever's under the pointer: a hand (`Pointer`) when Ctrl/Cmd
1102+ /// is held over a Ctrl-clickable link (same hit-test → `link_at` path as the click), else
1103+ /// the default arrow. Only calls `set_cursor` on a change. Gated on the modifier first so
1104+ /// the (snapshot-building) `link_at` lookup runs only during an actual Ctrl-hover.
1105+ fn update_link_cursor (
1106+ doc_engine : & mut DocEngine ,
1107+ modifiers : ModifiersState ,
1108+ mouse_pos : ( f32 , f32 ) ,
1109+ current : & mut CursorIcon ,
1110+ window : & Window ,
1111+ ) {
1112+ let over_link = ( modifiers. control_key ( ) || modifiers. super_key ( ) )
1113+ && doc_engine
1114+ . doc
1115+ . as_ref ( )
1116+ . and_then ( |d| d. hit_test ( mouse_pos. 0 , mouse_pos. 1 ) )
1117+ . is_some_and ( |off| doc_engine. editor . link_at ( off) . is_some ( ) ) ;
1118+ let icon = if over_link {
1119+ CursorIcon :: Pointer
1120+ } else {
1121+ CursorIcon :: Default
1122+ } ;
1123+ if icon != * current {
1124+ * current = icon;
1125+ window. set_cursor ( icon) ;
1126+ }
1127+ }
1128+
10971129/// Paint the clipped document body (diff backgrounds, quote gutters, rules, images,
10981130/// selection, glyphs, caret) plus the bottom status bar into `scene`. Overlays
10991131/// (hover popover / autocomplete) differ per caller and are drawn separately. Shared by
@@ -1564,6 +1596,14 @@ impl ApplicationHandler<WritEvent> for App {
15641596 }
15651597 WindowEvent :: ModifiersChanged ( mods) => {
15661598 self . modifiers = mods. state ( ) ;
1599+ // Pressing/releasing Ctrl while stationary over a link flips the pointer.
1600+ update_link_cursor (
1601+ & mut self . doc_engine ,
1602+ self . modifiers ,
1603+ self . mouse_pos ,
1604+ & mut self . cursor_icon ,
1605+ & state. window ,
1606+ ) ;
15671607 }
15681608 WindowEvent :: Ime ( winit:: event:: Ime :: Enabled ) => {
15691609 self . doc_engine . preedit = None ;
@@ -1729,6 +1769,16 @@ impl ApplicationHandler<WritEvent> for App {
17291769 state. window . request_redraw ( ) ;
17301770 }
17311771 }
1772+ // Ctrl-hover over a link shows the pointer cursor (skip while drag-selecting).
1773+ if !self . mouse_down {
1774+ update_link_cursor (
1775+ & mut self . doc_engine ,
1776+ self . modifiers ,
1777+ self . mouse_pos ,
1778+ & mut self . cursor_icon ,
1779+ & state. window ,
1780+ ) ;
1781+ }
17321782 }
17331783 WindowEvent :: MouseInput {
17341784 state : ElementState :: Pressed ,
0 commit comments