Fortran · 29481 bytes Raw Blame History
1 module window_mod
2 use, intrinsic :: iso_c_binding
3 use types
4 use glfw_bindings
5 use gl_bindings
6 use pty_mod
7 use terminal_mod
8 use selection_mod
9 implicit none
10 private
11
12 public :: window_t, selection_t
13 public :: window_create, window_destroy
14 public :: window_should_close, window_swap_buffers, window_poll_events
15 public :: window_get_size, window_set_pty, window_set_terminal
16 public :: window_set_title, window_set_cell_size, window_set_blur
17 public :: window_get_selection, window_clipboard_set, window_clipboard_get
18 public :: window_get_font_delta, window_clear_font_delta, window_set_font_size
19 public :: window_set_render_callback, window_is_resizing
20 public :: window_get_tab_action, window_clear_tab_action
21 public :: window_get_pane_action, window_clear_pane_action
22
23 ! Tab action constants
24 integer, parameter, public :: TAB_ACTION_NONE = 0
25 integer, parameter, public :: TAB_ACTION_NEW = 1
26 integer, parameter, public :: TAB_ACTION_CLOSE = 2
27 integer, parameter, public :: TAB_ACTION_NEXT = 3
28 integer, parameter, public :: TAB_ACTION_PREV = 4
29 ! TAB_ACTION_GOTO_1 through TAB_ACTION_GOTO_9 are 10-18
30
31 ! Pane action constants
32 integer, parameter, public :: PANE_ACTION_NONE = 0
33 integer, parameter, public :: PANE_ACTION_SPLIT_V = 1
34 integer, parameter, public :: PANE_ACTION_SPLIT_H = 2
35 integer, parameter, public :: PANE_ACTION_NAV_LEFT = 3
36 integer, parameter, public :: PANE_ACTION_NAV_RIGHT = 4
37 integer, parameter, public :: PANE_ACTION_NAV_UP = 5
38 integer, parameter, public :: PANE_ACTION_NAV_DOWN = 6
39
40 type :: window_t
41 type(c_ptr) :: handle = c_null_ptr
42 integer :: width = 0
43 integer :: height = 0
44 integer :: fb_width = 0
45 integer :: fb_height = 0
46 end type window_t
47
48 ! Module-level window pointer for callbacks
49 type(c_ptr), save :: current_window = c_null_ptr
50
51 ! Module-level PTY pointer for keyboard input
52 type(pty_t), pointer, save :: active_pty => null()
53
54 ! Module-level terminal pointer for scrollback
55 type(terminal_t), pointer, save :: active_term => null()
56
57 ! Selection state for mouse-based text selection
58 type(selection_t), save :: active_selection
59
60 ! Cell dimensions for mouse coordinate conversion
61 integer, save :: cell_width = 10
62 integer, save :: cell_height = 18
63
64 ! Font size adjustment state
65 integer, save :: pending_font_delta = 0 ! +2, -2, or -999 for reset
66 integer, save :: current_font_size = 16 ! Track current size
67
68 ! Tab action state
69 integer, save :: pending_tab_action = 0 ! 0=none, 1=new, 2=close, 3=next, 4=prev, 10-18=goto
70
71 ! Pane action state
72 integer, save :: pending_pane_action = 0 ! 0=none, 1=split_v, 2=split_h, 3-6=nav
73
74 ! Live resize rendering support
75 logical, save :: is_resizing = .false.
76 procedure(render_callback_interface), pointer, save :: render_callback => null()
77
78 ! Abstract interface for render callback
79 abstract interface
80 subroutine render_callback_interface()
81 end subroutine render_callback_interface
82 end interface
83
84 ! Interface to C helper for loading OpenGL
85 interface
86 integer(c_int) function fortty_load_gl() bind(C, name="fortty_load_gl")
87 import :: c_int
88 end function fortty_load_gl
89
90 ! macOS window blur (no-op on other platforms)
91 subroutine fortty_set_window_blur_c(window, enable) bind(C, name="fortty_set_window_blur")
92 import :: c_ptr, c_int
93 type(c_ptr), value :: window
94 integer(c_int), value :: enable
95 end subroutine fortty_set_window_blur_c
96 end interface
97
98 contains
99
100 function window_create(width, height, title, transparent) result(win)
101 integer, intent(in) :: width, height
102 character(len=*), intent(in) :: title
103 logical, intent(in), optional :: transparent
104 type(window_t) :: win
105 type(c_funptr) :: dummy
106 integer(c_int) :: gl_loaded
107 character(len=256) :: c_title
108
109 ! Initialize GLFW
110 if (glfwInit() == GLFW_FALSE) then
111 print *, "Error: Failed to initialize GLFW"
112 stop 1
113 end if
114
115 ! Set error callback
116 dummy = glfwSetErrorCallback(c_funloc(error_callback))
117
118 ! Request OpenGL 3.3 Core Profile
119 call glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3)
120 call glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3)
121 call glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)
122 call glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE)
123
124 ! Enable transparent framebuffer if requested (for window opacity)
125 if (present(transparent)) then
126 if (transparent) then
127 call glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE)
128 end if
129 end if
130
131 ! Create window
132 c_title = trim(title) // c_null_char
133 win%handle = glfwCreateWindow(width, height, c_title, c_null_ptr, c_null_ptr)
134
135 if (.not. c_associated(win%handle)) then
136 print *, "Error: Failed to create GLFW window"
137 call glfwTerminate()
138 stop 1
139 end if
140
141 win%width = width
142 win%height = height
143 current_window = win%handle
144
145 ! Make context current
146 call glfwMakeContextCurrent(win%handle)
147
148 ! Load OpenGL functions via GLAD
149 gl_loaded = fortty_load_gl()
150 if (gl_loaded == 0) then
151 print *, "Error: Failed to load OpenGL functions"
152 call glfwDestroyWindow(win%handle)
153 call glfwTerminate()
154 stop 1
155 end if
156
157 ! Get actual framebuffer size (may differ on HiDPI)
158 call glfwGetFramebufferSize(win%handle, win%fb_width, win%fb_height)
159
160 ! Set initial viewport
161 call glViewport(0, 0, win%fb_width, win%fb_height)
162
163 ! Register callbacks
164 dummy = glfwSetFramebufferSizeCallback(win%handle, c_funloc(framebuffer_size_callback))
165 dummy = glfwSetWindowRefreshCallback(win%handle, c_funloc(window_refresh_callback))
166 dummy = glfwSetKeyCallback(win%handle, c_funloc(key_callback))
167 dummy = glfwSetCharCallback(win%handle, c_funloc(char_callback))
168 dummy = glfwSetScrollCallback(win%handle, c_funloc(scroll_callback))
169 dummy = glfwSetMouseButtonCallback(win%handle, c_funloc(mouse_button_callback))
170 dummy = glfwSetCursorPosCallback(win%handle, c_funloc(cursor_pos_callback))
171
172 end function window_create
173
174 subroutine window_destroy(win)
175 type(window_t), intent(inout) :: win
176
177 if (c_associated(win%handle)) then
178 call glfwDestroyWindow(win%handle)
179 win%handle = c_null_ptr
180 end if
181
182 call glfwTerminate()
183 end subroutine window_destroy
184
185 function window_should_close(win) result(should)
186 type(window_t), intent(in) :: win
187 logical :: should
188
189 should = glfwWindowShouldClose(win%handle) /= GLFW_FALSE
190 end function window_should_close
191
192 subroutine window_swap_buffers(win)
193 type(window_t), intent(in) :: win
194
195 call glfwSwapBuffers(win%handle)
196 end subroutine window_swap_buffers
197
198 subroutine window_poll_events()
199 ! Use WaitEventsTimeout instead of PollEvents for Wayland compatibility
200 ! This allows the compositor to send ping-pong messages to verify the app
201 ! is responsive, even when on an inactive workspace. 10ms timeout provides
202 ! ~100Hz max update rate while allowing proper event processing.
203 call glfwWaitEventsTimeout(0.01d0)
204 end subroutine window_poll_events
205
206 ! Get current framebuffer size
207 subroutine window_get_size(win, width, height)
208 type(window_t), intent(in) :: win
209 integer, intent(out) :: width, height
210 integer(c_int) :: w, h
211
212 ! Use window size (points) not framebuffer size (pixels)
213 ! This ensures projection and cell math work correctly on HiDPI displays
214 call glfwGetWindowSize(win%handle, w, h)
215 width = int(w)
216 height = int(h)
217 end subroutine window_get_size
218
219 ! Callback: handle window resize
220 subroutine framebuffer_size_callback(window, width, height) bind(C)
221 type(c_ptr), value :: window
222 integer(c_int), value :: width, height
223
224 call glViewport(0, 0, width, height)
225
226 ! Mark that we're in a resize operation and trigger a redraw
227 ! This ensures smooth rendering during live resize on macOS
228 is_resizing = .true.
229 if (associated(render_callback)) then
230 call render_callback()
231 end if
232 end subroutine framebuffer_size_callback
233
234 ! Callback: handle window refresh (called when window needs redrawing)
235 subroutine window_refresh_callback(window) bind(C)
236 type(c_ptr), value :: window
237
238 ! Trigger a redraw via the render callback
239 if (associated(render_callback)) then
240 call render_callback()
241 end if
242 end subroutine window_refresh_callback
243
244 ! Set PTY for keyboard input
245 subroutine window_set_pty(p)
246 type(pty_t), target, intent(in) :: p
247
248 active_pty => p
249 end subroutine window_set_pty
250
251 ! Set terminal for scrollback control
252 subroutine window_set_terminal(t)
253 type(terminal_t), target, intent(in) :: t
254
255 active_term => t
256 end subroutine window_set_terminal
257
258 ! Callback: handle key presses (special keys and Ctrl combinations)
259 subroutine key_callback(window, key, scancode, action, mods) bind(C)
260 type(c_ptr), value :: window
261 integer(c_int), value :: key, scancode, action, mods
262 character(len=16) :: seq
263 integer :: seq_len
264
265 ! Only process press and repeat events
266 if (action == GLFW_RELEASE) return
267
268 ! Check if PTY is active
269 if (.not. associated(active_pty)) return
270
271 ! Close window on Escape (keep this for development)
272 if (key == GLFW_KEY_ESCAPE .and. action == GLFW_PRESS) then
273 call glfwSetWindowShouldClose(window, GLFW_TRUE)
274 return
275 end if
276
277 ! Handle Shift+PageUp/Down for scrollback (before resetting scroll)
278 if (associated(active_term) .and. iand(mods, GLFW_MOD_SHIFT) /= 0) then
279 if (key == GLFW_KEY_PAGE_UP) then
280 call terminal_scroll_view(active_term, active_term%rows)
281 return
282 else if (key == GLFW_KEY_PAGE_DOWN) then
283 call terminal_scroll_view(active_term, -active_term%rows)
284 return
285 end if
286 end if
287
288 ! Reset scroll view on any other key input (return to live view)
289 if (associated(active_term)) then
290 call terminal_reset_scroll_view(active_term)
291 end if
292
293 ! Handle Ctrl+Shift+V for paste
294 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .and. iand(mods, GLFW_MOD_SHIFT) /= 0) then
295 if (key == GLFW_KEY_V) then
296 call handle_paste(window)
297 return
298 end if
299 end if
300
301 ! Handle font size adjustment: Ctrl/Cmd + Plus/Minus/0
302 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .or. iand(mods, GLFW_MOD_SUPER) /= 0) then
303 if (key == GLFW_KEY_EQUAL .or. key == GLFW_KEY_KP_ADD) then
304 ! Ctrl/Cmd + = or numpad + (increase font size)
305 pending_font_delta = 2
306 return
307 else if (key == GLFW_KEY_MINUS .or. key == GLFW_KEY_KP_SUBTRACT) then
308 ! Ctrl/Cmd + - (decrease font size)
309 pending_font_delta = -2
310 return
311 else if (key == GLFW_KEY_0) then
312 ! Ctrl/Cmd + 0 (reset font size)
313 pending_font_delta = -999
314 return
315 end if
316 end if
317
318 ! Handle tab management: Ctrl/Cmd + T/W/[/]/1-9
319 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .or. iand(mods, GLFW_MOD_SUPER) /= 0) then
320 select case (key)
321 case (GLFW_KEY_T)
322 ! New tab
323 pending_tab_action = TAB_ACTION_NEW
324 return
325 case (GLFW_KEY_W)
326 ! Close tab
327 pending_tab_action = TAB_ACTION_CLOSE
328 return
329 case (GLFW_KEY_RIGHT_BRACKET)
330 ! Next tab (Cmd+])
331 pending_tab_action = TAB_ACTION_NEXT
332 return
333 case (GLFW_KEY_LEFT_BRACKET)
334 ! Previous tab (Cmd+[)
335 pending_tab_action = TAB_ACTION_PREV
336 return
337 case (GLFW_KEY_1)
338 pending_tab_action = 10
339 return
340 case (GLFW_KEY_2)
341 pending_tab_action = 11
342 return
343 case (GLFW_KEY_3)
344 pending_tab_action = 12
345 return
346 case (GLFW_KEY_4)
347 pending_tab_action = 13
348 return
349 case (GLFW_KEY_5)
350 pending_tab_action = 14
351 return
352 case (GLFW_KEY_6)
353 pending_tab_action = 15
354 return
355 case (GLFW_KEY_7)
356 pending_tab_action = 16
357 return
358 case (GLFW_KEY_8)
359 pending_tab_action = 17
360 return
361 case (GLFW_KEY_9)
362 pending_tab_action = 18
363 return
364
365 ! Pane splitting: Cmd/Ctrl + \ (vertical), Cmd/Ctrl + Shift + \ (horizontal)
366 case (GLFW_KEY_BACKSLASH)
367 if (iand(mods, GLFW_MOD_SHIFT) /= 0) then
368 pending_pane_action = PANE_ACTION_SPLIT_H
369 else
370 pending_pane_action = PANE_ACTION_SPLIT_V
371 end if
372 return
373
374 ! Pane navigation with arrow keys: Cmd/Ctrl + Arrow
375 case (GLFW_KEY_LEFT)
376 pending_pane_action = PANE_ACTION_NAV_LEFT
377 return
378 case (GLFW_KEY_RIGHT)
379 pending_pane_action = PANE_ACTION_NAV_RIGHT
380 return
381 case (GLFW_KEY_UP)
382 pending_pane_action = PANE_ACTION_NAV_UP
383 return
384 case (GLFW_KEY_DOWN)
385 pending_pane_action = PANE_ACTION_NAV_DOWN
386 return
387
388 end select
389 end if
390
391 ! Pane navigation with vim keys: Super/Cmd + hjkl only (not Ctrl)
392 ! This allows Ctrl+L (clear), Ctrl+H, etc. to pass through to the shell
393 if (iand(mods, GLFW_MOD_SUPER) /= 0 .and. iand(mods, GLFW_MOD_CONTROL) == 0) then
394 select case (key)
395 case (GLFW_KEY_H)
396 pending_pane_action = PANE_ACTION_NAV_LEFT
397 return
398 case (GLFW_KEY_L)
399 pending_pane_action = PANE_ACTION_NAV_RIGHT
400 return
401 case (GLFW_KEY_K)
402 pending_pane_action = PANE_ACTION_NAV_UP
403 return
404 case (GLFW_KEY_J)
405 pending_pane_action = PANE_ACTION_NAV_DOWN
406 return
407 end select
408 end if
409
410 ! Handle Ctrl combinations (these don't trigger char_callback)
411 if (iand(mods, GLFW_MOD_CONTROL) /= 0) then
412 if (key >= GLFW_KEY_A .and. key <= GLFW_KEY_Z) then
413 ! Ctrl+A through Ctrl+Z = 0x01-0x1A
414 seq(1:1) = char(key - GLFW_KEY_A + 1)
415 call pty_write(active_pty, seq, 1)
416 return
417 end if
418 end if
419
420 ! Handle special keys
421 seq_len = 0
422 select case (key)
423 case (GLFW_KEY_ENTER)
424 seq(1:1) = char(13) ! CR
425 seq_len = 1
426
427 case (GLFW_KEY_TAB)
428 if (iand(mods, GLFW_MOD_SHIFT) /= 0) then
429 ! Shift+Tab = CSI Z
430 seq = char(27) // '[Z'
431 seq_len = 3
432 else
433 seq(1:1) = char(9) ! Tab
434 seq_len = 1
435 end if
436
437 case (GLFW_KEY_BACKSPACE)
438 seq(1:1) = char(127) ! DEL (most terminals expect this)
439 seq_len = 1
440
441 case (GLFW_KEY_UP)
442 call build_arrow_seq(seq, seq_len, 'A', mods)
443
444 case (GLFW_KEY_DOWN)
445 call build_arrow_seq(seq, seq_len, 'B', mods)
446
447 case (GLFW_KEY_RIGHT)
448 call build_arrow_seq(seq, seq_len, 'C', mods)
449
450 case (GLFW_KEY_LEFT)
451 call build_arrow_seq(seq, seq_len, 'D', mods)
452
453 case (GLFW_KEY_HOME)
454 call build_tilde_seq(seq, seq_len, 1, mods)
455
456 case (GLFW_KEY_END)
457 call build_tilde_seq(seq, seq_len, 4, mods)
458
459 case (GLFW_KEY_INSERT)
460 call build_tilde_seq(seq, seq_len, 2, mods)
461
462 case (GLFW_KEY_DELETE)
463 call build_tilde_seq(seq, seq_len, 3, mods)
464
465 case (GLFW_KEY_PAGE_UP)
466 call build_tilde_seq(seq, seq_len, 5, mods)
467
468 case (GLFW_KEY_PAGE_DOWN)
469 call build_tilde_seq(seq, seq_len, 6, mods)
470
471 case (GLFW_KEY_F1)
472 call build_fkey_seq(seq, seq_len, 11, mods)
473 case (GLFW_KEY_F2)
474 call build_fkey_seq(seq, seq_len, 12, mods)
475 case (GLFW_KEY_F3)
476 call build_fkey_seq(seq, seq_len, 13, mods)
477 case (GLFW_KEY_F4)
478 call build_fkey_seq(seq, seq_len, 14, mods)
479 case (GLFW_KEY_F5)
480 call build_fkey_seq(seq, seq_len, 15, mods)
481 case (GLFW_KEY_F6)
482 call build_fkey_seq(seq, seq_len, 17, mods)
483 case (GLFW_KEY_F7)
484 call build_fkey_seq(seq, seq_len, 18, mods)
485 case (GLFW_KEY_F8)
486 call build_fkey_seq(seq, seq_len, 19, mods)
487 case (GLFW_KEY_F9)
488 call build_fkey_seq(seq, seq_len, 20, mods)
489 case (GLFW_KEY_F10)
490 call build_fkey_seq(seq, seq_len, 21, mods)
491 case (GLFW_KEY_F11)
492 call build_fkey_seq(seq, seq_len, 23, mods)
493 case (GLFW_KEY_F12)
494 call build_fkey_seq(seq, seq_len, 24, mods)
495 end select
496
497 if (seq_len > 0) then
498 call pty_write(active_pty, seq, seq_len)
499 end if
500 end subroutine key_callback
501
502 ! Callback: handle character input (regular text)
503 subroutine char_callback(window, codepoint) bind(C)
504 type(c_ptr), value :: window
505 integer(c_int), value :: codepoint
506 character(len=4) :: utf8
507 integer :: utf8_len
508
509 if (.not. associated(active_pty)) return
510
511 ! Convert Unicode codepoint to UTF-8
512 call codepoint_to_utf8(codepoint, utf8, utf8_len)
513
514 if (utf8_len > 0) then
515 call pty_write(active_pty, utf8, utf8_len)
516 end if
517 end subroutine char_callback
518
519 ! Build arrow key escape sequence
520 subroutine build_arrow_seq(seq, seq_len, letter, mods)
521 character(len=*), intent(out) :: seq
522 integer, intent(out) :: seq_len
523 character(len=1), intent(in) :: letter
524 integer(c_int), intent(in) :: mods
525 integer :: mod_num
526
527 mod_num = get_modifier_num(mods)
528
529 if (mod_num == 0) then
530 ! ESC [ <letter>
531 seq = char(27) // '[' // letter
532 seq_len = 3
533 else
534 ! ESC [ 1 ; <mod> <letter>
535 seq = char(27) // '[1;' // char(mod_num + ichar('0')) // letter
536 seq_len = 6
537 end if
538 end subroutine build_arrow_seq
539
540 ! Build tilde-terminated escape sequence (Home, End, Insert, Delete, PgUp, PgDn)
541 subroutine build_tilde_seq(seq, seq_len, code, mods)
542 character(len=*), intent(out) :: seq
543 integer, intent(out) :: seq_len
544 integer, intent(in) :: code
545 integer(c_int), intent(in) :: mods
546 integer :: mod_num
547 character(len=2) :: code_str
548
549 mod_num = get_modifier_num(mods)
550 write(code_str, '(I1)') code
551
552 if (mod_num == 0) then
553 ! ESC [ <code> ~
554 seq = char(27) // '[' // trim(code_str) // '~'
555 seq_len = 4
556 else
557 ! ESC [ <code> ; <mod> ~
558 seq = char(27) // '[' // trim(code_str) // ';' // char(mod_num + ichar('0')) // '~'
559 seq_len = 6
560 end if
561 end subroutine build_tilde_seq
562
563 ! Build function key escape sequence
564 subroutine build_fkey_seq(seq, seq_len, code, mods)
565 character(len=*), intent(out) :: seq
566 integer, intent(out) :: seq_len
567 integer, intent(in) :: code
568 integer(c_int), intent(in) :: mods
569 integer :: mod_num
570 character(len=2) :: code_str
571
572 mod_num = get_modifier_num(mods)
573 write(code_str, '(I2)') code
574
575 if (mod_num == 0) then
576 ! ESC [ <code> ~
577 seq = char(27) // '[' // trim(code_str) // '~'
578 seq_len = 5
579 else
580 ! ESC [ <code> ; <mod> ~
581 seq = char(27) // '[' // trim(code_str) // ';' // char(mod_num + ichar('0')) // '~'
582 seq_len = 7
583 end if
584 end subroutine build_fkey_seq
585
586 ! Convert GLFW modifier bits to xterm modifier number
587 function get_modifier_num(mods) result(mod_num)
588 integer(c_int), intent(in) :: mods
589 integer :: mod_num
590
591 mod_num = 0
592 if (iand(mods, GLFW_MOD_SHIFT) /= 0) mod_num = mod_num + 1
593 if (iand(mods, GLFW_MOD_ALT) /= 0) mod_num = mod_num + 2
594 if (iand(mods, GLFW_MOD_CONTROL) /= 0) mod_num = mod_num + 4
595
596 ! xterm encoding: modifier = 1 + shift + alt*2 + ctrl*4
597 if (mod_num > 0) mod_num = mod_num + 1
598 end function get_modifier_num
599
600 ! Convert Unicode codepoint to UTF-8 bytes
601 subroutine codepoint_to_utf8(cp, utf8, length)
602 integer(c_int), intent(in) :: cp
603 character(len=*), intent(out) :: utf8
604 integer, intent(out) :: length
605
606 if (cp < 128) then
607 ! ASCII
608 utf8(1:1) = char(cp)
609 length = 1
610 else if (cp < 2048) then
611 ! 2-byte UTF-8
612 utf8(1:1) = char(ior(192, ishft(cp, -6)))
613 utf8(2:2) = char(ior(128, iand(cp, 63)))
614 length = 2
615 else if (cp < 65536) then
616 ! 3-byte UTF-8
617 utf8(1:1) = char(ior(224, ishft(cp, -12)))
618 utf8(2:2) = char(ior(128, iand(ishft(cp, -6), 63)))
619 utf8(3:3) = char(ior(128, iand(cp, 63)))
620 length = 3
621 else if (cp < 1114112) then
622 ! 4-byte UTF-8
623 utf8(1:1) = char(ior(240, ishft(cp, -18)))
624 utf8(2:2) = char(ior(128, iand(ishft(cp, -12), 63)))
625 utf8(3:3) = char(ior(128, iand(ishft(cp, -6), 63)))
626 utf8(4:4) = char(ior(128, iand(cp, 63)))
627 length = 4
628 else
629 ! Invalid codepoint
630 length = 0
631 end if
632 end subroutine codepoint_to_utf8
633
634 ! Callback: handle GLFW errors
635 subroutine error_callback(error_code, description) bind(C)
636 integer(c_int), value :: error_code
637 type(c_ptr), value :: description
638
639 print *, "GLFW Error ", error_code
640 end subroutine error_callback
641
642 ! Callback: handle mouse scroll wheel
643 subroutine scroll_callback(window, xoffset, yoffset) bind(C)
644 type(c_ptr), value :: window
645 real(c_double), value :: xoffset, yoffset
646 integer :: scroll_lines
647
648 if (.not. associated(active_term)) return
649
650 ! Convert scroll amount to lines (typically 3 lines per notch)
651 scroll_lines = nint(yoffset * 3.0d0)
652
653 ! Positive yoffset = scroll up (back in history)
654 call terminal_scroll_view(active_term, scroll_lines)
655 end subroutine scroll_callback
656
657 ! Set window title
658 subroutine window_set_title(win, title)
659 type(window_t), intent(in) :: win
660 character(len=*), intent(in) :: title
661 character(len=257) :: c_title
662
663 if (.not. c_associated(win%handle)) return
664
665 c_title = trim(title) // c_null_char
666 call glfwSetWindowTitle(win%handle, c_title)
667 end subroutine window_set_title
668
669 ! Enable background blur (macOS only, no-op on other platforms)
670 subroutine window_set_blur(win, enable)
671 type(window_t), intent(in) :: win
672 logical, intent(in) :: enable
673
674 if (.not. c_associated(win%handle)) return
675
676 if (enable) then
677 call fortty_set_window_blur_c(win%handle, 1_c_int)
678 else
679 call fortty_set_window_blur_c(win%handle, 0_c_int)
680 end if
681 end subroutine window_set_blur
682
683 ! Set cell dimensions for mouse coordinate conversion
684 subroutine window_set_cell_size(w, h)
685 integer, intent(in) :: w, h
686
687 cell_width = w
688 cell_height = h
689 end subroutine window_set_cell_size
690
691 ! Get selection state (for rendering)
692 function window_get_selection() result(sel)
693 type(selection_t) :: sel
694
695 sel = active_selection
696 end function window_get_selection
697
698 ! Set clipboard content
699 subroutine window_clipboard_set(win, text)
700 type(window_t), intent(in) :: win
701 character(len=*), intent(in) :: text
702 character(len=4097) :: c_text
703
704 if (.not. c_associated(win%handle)) return
705 if (len_trim(text) == 0) return
706
707 c_text = trim(text) // c_null_char
708 call glfwSetClipboardString(win%handle, c_text)
709 end subroutine window_clipboard_set
710
711 ! Get clipboard content
712 function window_clipboard_get(win) result(text)
713 type(window_t), intent(in) :: win
714 character(len=4096) :: text
715 type(c_ptr) :: clip_ptr
716 character(len=1), pointer :: chars(:)
717 integer :: i, length
718
719 text = ''
720 if (.not. c_associated(win%handle)) return
721
722 clip_ptr = glfwGetClipboardString(win%handle)
723 if (.not. c_associated(clip_ptr)) return
724
725 ! Convert C string to Fortran string
726 call c_f_pointer(clip_ptr, chars, [4096])
727 length = 0
728 do i = 1, 4096
729 if (chars(i) == c_null_char) exit
730 length = i
731 end do
732
733 do i = 1, min(length, 4096)
734 text(i:i) = chars(i)
735 end do
736 end function window_clipboard_get
737
738 ! Callback: handle mouse button events
739 subroutine mouse_button_callback(window, button, action, mods) bind(C)
740 type(c_ptr), value :: window
741 integer(c_int), value :: button, action, mods
742 real(c_double) :: xpos, ypos
743 integer :: col, row
744
745 ! Only handle left mouse button for selection
746 if (button /= GLFW_MOUSE_BUTTON_LEFT) return
747
748 call glfwGetCursorPos(window, xpos, ypos)
749
750 ! Convert pixel position to terminal cell coordinates (1-based)
751 col = int(xpos / cell_width) + 1
752 row = int(ypos / cell_height) + 1
753
754 ! Clamp to valid range
755 if (associated(active_term)) then
756 if (col < 1) col = 1
757 if (col > active_term%cols) col = active_term%cols
758 if (row < 1) row = 1
759 if (row > active_term%rows) row = active_term%rows
760 end if
761
762 if (action == GLFW_PRESS) then
763 call selection_start(active_selection, row, col)
764 else if (action == GLFW_RELEASE) then
765 call selection_end(active_selection)
766 ! Copy selection to clipboard if active
767 if (selection_is_active(active_selection)) then
768 call copy_selection_to_clipboard(window)
769 end if
770 end if
771 end subroutine mouse_button_callback
772
773 ! Callback: handle cursor position changes
774 subroutine cursor_pos_callback(window, xpos, ypos) bind(C)
775 type(c_ptr), value :: window
776 real(c_double), value :: xpos, ypos
777 integer :: col, row
778
779 ! Only update if actively selecting
780 if (.not. active_selection%selecting) return
781
782 ! Convert pixel position to terminal cell coordinates (1-based)
783 col = int(xpos / cell_width) + 1
784 row = int(ypos / cell_height) + 1
785
786 ! Clamp to valid range
787 if (associated(active_term)) then
788 if (col < 1) col = 1
789 if (col > active_term%cols) col = active_term%cols
790 if (row < 1) row = 1
791 if (row > active_term%rows) row = active_term%rows
792 end if
793
794 call selection_update(active_selection, row, col)
795 end subroutine cursor_pos_callback
796
797 ! Copy selection to clipboard
798 subroutine copy_selection_to_clipboard(window)
799 use screen_mod
800 use cell_mod
801 type(c_ptr), intent(in) :: window
802 type(screen_t), pointer :: scr
803 type(cell_t) :: cell
804 character(len=4096) :: text
805 character(len=4) :: utf8
806 integer :: r1, c1, r2, c2, row, col, pos, utf8_len
807
808 if (.not. associated(active_term)) return
809 if (.not. selection_is_active(active_selection)) return
810
811 scr => terminal_active_screen(active_term)
812 call selection_get_bounds(active_selection, r1, c1, r2, c2)
813
814 text = ''
815 pos = 1
816
817 do row = r1, r2
818 do col = 1, scr%cols
819 ! Check if this cell is in selection
820 if (.not. selection_contains(active_selection, row, col)) cycle
821
822 cell = screen_get_cell(scr, row, col)
823
824 ! Convert codepoint to UTF-8
825 if (cell%codepoint >= 32 .and. cell%codepoint < 1114112) then
826 call codepoint_to_utf8(cell%codepoint, utf8, utf8_len)
827 if (pos + utf8_len - 1 <= 4096) then
828 text(pos:pos+utf8_len-1) = utf8(1:utf8_len)
829 pos = pos + utf8_len
830 end if
831 end if
832 end do
833
834 ! Add newline between rows (except last row)
835 if (row < r2 .and. pos < 4096) then
836 text(pos:pos) = char(10)
837 pos = pos + 1
838 end if
839 end do
840
841 ! Set clipboard
842 if (pos > 1) then
843 call glfwSetClipboardString(window, trim(text(1:pos-1)) // c_null_char)
844 end if
845 end subroutine copy_selection_to_clipboard
846
847 ! Handle paste from clipboard
848 subroutine handle_paste(window)
849 type(c_ptr), intent(in) :: window
850 type(c_ptr) :: clip_ptr
851 character(len=1), pointer :: chars(:)
852 integer :: i, length
853
854 if (.not. associated(active_pty)) return
855
856 clip_ptr = glfwGetClipboardString(window)
857 if (.not. c_associated(clip_ptr)) return
858
859 ! Find length of C string
860 call c_f_pointer(clip_ptr, chars, [4096])
861 length = 0
862 do i = 1, 4096
863 if (chars(i) == c_null_char) exit
864 length = i
865 end do
866
867 ! Write to PTY
868 if (length > 0) then
869 block
870 character(len=4096) :: paste_text
871 integer :: j
872 paste_text = ''
873 do j = 1, length
874 paste_text(j:j) = chars(j)
875 end do
876 call pty_write(active_pty, paste_text, length)
877 end block
878 end if
879 end subroutine handle_paste
880
881 ! Get pending font size delta (returns 0 if none)
882 function window_get_font_delta() result(delta)
883 integer :: delta
884 delta = pending_font_delta
885 end function window_get_font_delta
886
887 ! Clear pending font size delta
888 subroutine window_clear_font_delta()
889 pending_font_delta = 0
890 end subroutine window_clear_font_delta
891
892 ! Set current font size (for tracking)
893 subroutine window_set_font_size(size)
894 integer, intent(in) :: size
895 current_font_size = size
896 end subroutine window_set_font_size
897
898 ! Set the render callback for live resize support
899 subroutine window_set_render_callback(callback)
900 procedure(render_callback_interface) :: callback
901
902 render_callback => callback
903 end subroutine window_set_render_callback
904
905 ! Check if window is currently resizing (for main loop optimization)
906 function window_is_resizing() result(resizing)
907 logical :: resizing
908 resizing = is_resizing
909 is_resizing = .false. ! Clear the flag after reading
910 end function window_is_resizing
911
912 ! Get pending tab action (returns 0 if none)
913 function window_get_tab_action() result(action)
914 integer :: action
915 action = pending_tab_action
916 end function window_get_tab_action
917
918 ! Clear pending tab action
919 subroutine window_clear_tab_action()
920 pending_tab_action = TAB_ACTION_NONE
921 end subroutine window_clear_tab_action
922
923 ! Get pending pane action (returns 0 if none)
924 function window_get_pane_action() result(action)
925 integer :: action
926 action = pending_pane_action
927 end function window_get_pane_action
928
929 ! Clear pending pane action
930 subroutine window_clear_pane_action()
931 pending_pane_action = PANE_ACTION_NONE
932 end subroutine window_clear_pane_action
933
934 end module window_mod
935