Fortran · 29166 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 call glfwPollEvents()
200 end subroutine window_poll_events
201
202 ! Get current framebuffer size
203 subroutine window_get_size(win, width, height)
204 type(window_t), intent(in) :: win
205 integer, intent(out) :: width, height
206 integer(c_int) :: w, h
207
208 ! Use window size (points) not framebuffer size (pixels)
209 ! This ensures projection and cell math work correctly on HiDPI displays
210 call glfwGetWindowSize(win%handle, w, h)
211 width = int(w)
212 height = int(h)
213 end subroutine window_get_size
214
215 ! Callback: handle window resize
216 subroutine framebuffer_size_callback(window, width, height) bind(C)
217 type(c_ptr), value :: window
218 integer(c_int), value :: width, height
219
220 call glViewport(0, 0, width, height)
221
222 ! Mark that we're in a resize operation and trigger a redraw
223 ! This ensures smooth rendering during live resize on macOS
224 is_resizing = .true.
225 if (associated(render_callback)) then
226 call render_callback()
227 end if
228 end subroutine framebuffer_size_callback
229
230 ! Callback: handle window refresh (called when window needs redrawing)
231 subroutine window_refresh_callback(window) bind(C)
232 type(c_ptr), value :: window
233
234 ! Trigger a redraw via the render callback
235 if (associated(render_callback)) then
236 call render_callback()
237 end if
238 end subroutine window_refresh_callback
239
240 ! Set PTY for keyboard input
241 subroutine window_set_pty(p)
242 type(pty_t), target, intent(in) :: p
243
244 active_pty => p
245 end subroutine window_set_pty
246
247 ! Set terminal for scrollback control
248 subroutine window_set_terminal(t)
249 type(terminal_t), target, intent(in) :: t
250
251 active_term => t
252 end subroutine window_set_terminal
253
254 ! Callback: handle key presses (special keys and Ctrl combinations)
255 subroutine key_callback(window, key, scancode, action, mods) bind(C)
256 type(c_ptr), value :: window
257 integer(c_int), value :: key, scancode, action, mods
258 character(len=16) :: seq
259 integer :: seq_len
260
261 ! Only process press and repeat events
262 if (action == GLFW_RELEASE) return
263
264 ! Check if PTY is active
265 if (.not. associated(active_pty)) return
266
267 ! Close window on Escape (keep this for development)
268 if (key == GLFW_KEY_ESCAPE .and. action == GLFW_PRESS) then
269 call glfwSetWindowShouldClose(window, GLFW_TRUE)
270 return
271 end if
272
273 ! Handle Shift+PageUp/Down for scrollback (before resetting scroll)
274 if (associated(active_term) .and. iand(mods, GLFW_MOD_SHIFT) /= 0) then
275 if (key == GLFW_KEY_PAGE_UP) then
276 call terminal_scroll_view(active_term, active_term%rows)
277 return
278 else if (key == GLFW_KEY_PAGE_DOWN) then
279 call terminal_scroll_view(active_term, -active_term%rows)
280 return
281 end if
282 end if
283
284 ! Reset scroll view on any other key input (return to live view)
285 if (associated(active_term)) then
286 call terminal_reset_scroll_view(active_term)
287 end if
288
289 ! Handle Ctrl+Shift+V for paste
290 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .and. iand(mods, GLFW_MOD_SHIFT) /= 0) then
291 if (key == GLFW_KEY_V) then
292 call handle_paste(window)
293 return
294 end if
295 end if
296
297 ! Handle font size adjustment: Ctrl/Cmd + Plus/Minus/0
298 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .or. iand(mods, GLFW_MOD_SUPER) /= 0) then
299 if (key == GLFW_KEY_EQUAL .or. key == GLFW_KEY_KP_ADD) then
300 ! Ctrl/Cmd + = or numpad + (increase font size)
301 pending_font_delta = 2
302 return
303 else if (key == GLFW_KEY_MINUS .or. key == GLFW_KEY_KP_SUBTRACT) then
304 ! Ctrl/Cmd + - (decrease font size)
305 pending_font_delta = -2
306 return
307 else if (key == GLFW_KEY_0) then
308 ! Ctrl/Cmd + 0 (reset font size)
309 pending_font_delta = -999
310 return
311 end if
312 end if
313
314 ! Handle tab management: Ctrl/Cmd + T/W/[/]/1-9
315 if (iand(mods, GLFW_MOD_CONTROL) /= 0 .or. iand(mods, GLFW_MOD_SUPER) /= 0) then
316 select case (key)
317 case (GLFW_KEY_T)
318 ! New tab
319 pending_tab_action = TAB_ACTION_NEW
320 return
321 case (GLFW_KEY_W)
322 ! Close tab
323 pending_tab_action = TAB_ACTION_CLOSE
324 return
325 case (GLFW_KEY_RIGHT_BRACKET)
326 ! Next tab (Cmd+])
327 pending_tab_action = TAB_ACTION_NEXT
328 return
329 case (GLFW_KEY_LEFT_BRACKET)
330 ! Previous tab (Cmd+[)
331 pending_tab_action = TAB_ACTION_PREV
332 return
333 case (GLFW_KEY_1)
334 pending_tab_action = 10
335 return
336 case (GLFW_KEY_2)
337 pending_tab_action = 11
338 return
339 case (GLFW_KEY_3)
340 pending_tab_action = 12
341 return
342 case (GLFW_KEY_4)
343 pending_tab_action = 13
344 return
345 case (GLFW_KEY_5)
346 pending_tab_action = 14
347 return
348 case (GLFW_KEY_6)
349 pending_tab_action = 15
350 return
351 case (GLFW_KEY_7)
352 pending_tab_action = 16
353 return
354 case (GLFW_KEY_8)
355 pending_tab_action = 17
356 return
357 case (GLFW_KEY_9)
358 pending_tab_action = 18
359 return
360
361 ! Pane splitting: Cmd/Ctrl + \ (vertical), Cmd/Ctrl + Shift + \ (horizontal)
362 case (GLFW_KEY_BACKSLASH)
363 if (iand(mods, GLFW_MOD_SHIFT) /= 0) then
364 pending_pane_action = PANE_ACTION_SPLIT_H
365 else
366 pending_pane_action = PANE_ACTION_SPLIT_V
367 end if
368 return
369
370 ! Pane navigation with arrow keys: Cmd/Ctrl + Arrow
371 case (GLFW_KEY_LEFT)
372 pending_pane_action = PANE_ACTION_NAV_LEFT
373 return
374 case (GLFW_KEY_RIGHT)
375 pending_pane_action = PANE_ACTION_NAV_RIGHT
376 return
377 case (GLFW_KEY_UP)
378 pending_pane_action = PANE_ACTION_NAV_UP
379 return
380 case (GLFW_KEY_DOWN)
381 pending_pane_action = PANE_ACTION_NAV_DOWN
382 return
383
384 end select
385 end if
386
387 ! Pane navigation with vim keys: Super/Cmd + hjkl only (not Ctrl)
388 ! This allows Ctrl+L (clear), Ctrl+H, etc. to pass through to the shell
389 if (iand(mods, GLFW_MOD_SUPER) /= 0 .and. iand(mods, GLFW_MOD_CONTROL) == 0) then
390 select case (key)
391 case (GLFW_KEY_H)
392 pending_pane_action = PANE_ACTION_NAV_LEFT
393 return
394 case (GLFW_KEY_L)
395 pending_pane_action = PANE_ACTION_NAV_RIGHT
396 return
397 case (GLFW_KEY_K)
398 pending_pane_action = PANE_ACTION_NAV_UP
399 return
400 case (GLFW_KEY_J)
401 pending_pane_action = PANE_ACTION_NAV_DOWN
402 return
403 end select
404 end if
405
406 ! Handle Ctrl combinations (these don't trigger char_callback)
407 if (iand(mods, GLFW_MOD_CONTROL) /= 0) then
408 if (key >= GLFW_KEY_A .and. key <= GLFW_KEY_Z) then
409 ! Ctrl+A through Ctrl+Z = 0x01-0x1A
410 seq(1:1) = char(key - GLFW_KEY_A + 1)
411 call pty_write(active_pty, seq, 1)
412 return
413 end if
414 end if
415
416 ! Handle special keys
417 seq_len = 0
418 select case (key)
419 case (GLFW_KEY_ENTER)
420 seq(1:1) = char(13) ! CR
421 seq_len = 1
422
423 case (GLFW_KEY_TAB)
424 if (iand(mods, GLFW_MOD_SHIFT) /= 0) then
425 ! Shift+Tab = CSI Z
426 seq = char(27) // '[Z'
427 seq_len = 3
428 else
429 seq(1:1) = char(9) ! Tab
430 seq_len = 1
431 end if
432
433 case (GLFW_KEY_BACKSPACE)
434 seq(1:1) = char(127) ! DEL (most terminals expect this)
435 seq_len = 1
436
437 case (GLFW_KEY_UP)
438 call build_arrow_seq(seq, seq_len, 'A', mods)
439
440 case (GLFW_KEY_DOWN)
441 call build_arrow_seq(seq, seq_len, 'B', mods)
442
443 case (GLFW_KEY_RIGHT)
444 call build_arrow_seq(seq, seq_len, 'C', mods)
445
446 case (GLFW_KEY_LEFT)
447 call build_arrow_seq(seq, seq_len, 'D', mods)
448
449 case (GLFW_KEY_HOME)
450 call build_tilde_seq(seq, seq_len, 1, mods)
451
452 case (GLFW_KEY_END)
453 call build_tilde_seq(seq, seq_len, 4, mods)
454
455 case (GLFW_KEY_INSERT)
456 call build_tilde_seq(seq, seq_len, 2, mods)
457
458 case (GLFW_KEY_DELETE)
459 call build_tilde_seq(seq, seq_len, 3, mods)
460
461 case (GLFW_KEY_PAGE_UP)
462 call build_tilde_seq(seq, seq_len, 5, mods)
463
464 case (GLFW_KEY_PAGE_DOWN)
465 call build_tilde_seq(seq, seq_len, 6, mods)
466
467 case (GLFW_KEY_F1)
468 call build_fkey_seq(seq, seq_len, 11, mods)
469 case (GLFW_KEY_F2)
470 call build_fkey_seq(seq, seq_len, 12, mods)
471 case (GLFW_KEY_F3)
472 call build_fkey_seq(seq, seq_len, 13, mods)
473 case (GLFW_KEY_F4)
474 call build_fkey_seq(seq, seq_len, 14, mods)
475 case (GLFW_KEY_F5)
476 call build_fkey_seq(seq, seq_len, 15, mods)
477 case (GLFW_KEY_F6)
478 call build_fkey_seq(seq, seq_len, 17, mods)
479 case (GLFW_KEY_F7)
480 call build_fkey_seq(seq, seq_len, 18, mods)
481 case (GLFW_KEY_F8)
482 call build_fkey_seq(seq, seq_len, 19, mods)
483 case (GLFW_KEY_F9)
484 call build_fkey_seq(seq, seq_len, 20, mods)
485 case (GLFW_KEY_F10)
486 call build_fkey_seq(seq, seq_len, 21, mods)
487 case (GLFW_KEY_F11)
488 call build_fkey_seq(seq, seq_len, 23, mods)
489 case (GLFW_KEY_F12)
490 call build_fkey_seq(seq, seq_len, 24, mods)
491 end select
492
493 if (seq_len > 0) then
494 call pty_write(active_pty, seq, seq_len)
495 end if
496 end subroutine key_callback
497
498 ! Callback: handle character input (regular text)
499 subroutine char_callback(window, codepoint) bind(C)
500 type(c_ptr), value :: window
501 integer(c_int), value :: codepoint
502 character(len=4) :: utf8
503 integer :: utf8_len
504
505 if (.not. associated(active_pty)) return
506
507 ! Convert Unicode codepoint to UTF-8
508 call codepoint_to_utf8(codepoint, utf8, utf8_len)
509
510 if (utf8_len > 0) then
511 call pty_write(active_pty, utf8, utf8_len)
512 end if
513 end subroutine char_callback
514
515 ! Build arrow key escape sequence
516 subroutine build_arrow_seq(seq, seq_len, letter, mods)
517 character(len=*), intent(out) :: seq
518 integer, intent(out) :: seq_len
519 character(len=1), intent(in) :: letter
520 integer(c_int), intent(in) :: mods
521 integer :: mod_num
522
523 mod_num = get_modifier_num(mods)
524
525 if (mod_num == 0) then
526 ! ESC [ <letter>
527 seq = char(27) // '[' // letter
528 seq_len = 3
529 else
530 ! ESC [ 1 ; <mod> <letter>
531 seq = char(27) // '[1;' // char(mod_num + ichar('0')) // letter
532 seq_len = 6
533 end if
534 end subroutine build_arrow_seq
535
536 ! Build tilde-terminated escape sequence (Home, End, Insert, Delete, PgUp, PgDn)
537 subroutine build_tilde_seq(seq, seq_len, code, mods)
538 character(len=*), intent(out) :: seq
539 integer, intent(out) :: seq_len
540 integer, intent(in) :: code
541 integer(c_int), intent(in) :: mods
542 integer :: mod_num
543 character(len=2) :: code_str
544
545 mod_num = get_modifier_num(mods)
546 write(code_str, '(I1)') code
547
548 if (mod_num == 0) then
549 ! ESC [ <code> ~
550 seq = char(27) // '[' // trim(code_str) // '~'
551 seq_len = 4
552 else
553 ! ESC [ <code> ; <mod> ~
554 seq = char(27) // '[' // trim(code_str) // ';' // char(mod_num + ichar('0')) // '~'
555 seq_len = 6
556 end if
557 end subroutine build_tilde_seq
558
559 ! Build function key escape sequence
560 subroutine build_fkey_seq(seq, seq_len, code, mods)
561 character(len=*), intent(out) :: seq
562 integer, intent(out) :: seq_len
563 integer, intent(in) :: code
564 integer(c_int), intent(in) :: mods
565 integer :: mod_num
566 character(len=2) :: code_str
567
568 mod_num = get_modifier_num(mods)
569 write(code_str, '(I2)') code
570
571 if (mod_num == 0) then
572 ! ESC [ <code> ~
573 seq = char(27) // '[' // trim(code_str) // '~'
574 seq_len = 5
575 else
576 ! ESC [ <code> ; <mod> ~
577 seq = char(27) // '[' // trim(code_str) // ';' // char(mod_num + ichar('0')) // '~'
578 seq_len = 7
579 end if
580 end subroutine build_fkey_seq
581
582 ! Convert GLFW modifier bits to xterm modifier number
583 function get_modifier_num(mods) result(mod_num)
584 integer(c_int), intent(in) :: mods
585 integer :: mod_num
586
587 mod_num = 0
588 if (iand(mods, GLFW_MOD_SHIFT) /= 0) mod_num = mod_num + 1
589 if (iand(mods, GLFW_MOD_ALT) /= 0) mod_num = mod_num + 2
590 if (iand(mods, GLFW_MOD_CONTROL) /= 0) mod_num = mod_num + 4
591
592 ! xterm encoding: modifier = 1 + shift + alt*2 + ctrl*4
593 if (mod_num > 0) mod_num = mod_num + 1
594 end function get_modifier_num
595
596 ! Convert Unicode codepoint to UTF-8 bytes
597 subroutine codepoint_to_utf8(cp, utf8, length)
598 integer(c_int), intent(in) :: cp
599 character(len=*), intent(out) :: utf8
600 integer, intent(out) :: length
601
602 if (cp < 128) then
603 ! ASCII
604 utf8(1:1) = char(cp)
605 length = 1
606 else if (cp < 2048) then
607 ! 2-byte UTF-8
608 utf8(1:1) = char(ior(192, ishft(cp, -6)))
609 utf8(2:2) = char(ior(128, iand(cp, 63)))
610 length = 2
611 else if (cp < 65536) then
612 ! 3-byte UTF-8
613 utf8(1:1) = char(ior(224, ishft(cp, -12)))
614 utf8(2:2) = char(ior(128, iand(ishft(cp, -6), 63)))
615 utf8(3:3) = char(ior(128, iand(cp, 63)))
616 length = 3
617 else if (cp < 1114112) then
618 ! 4-byte UTF-8
619 utf8(1:1) = char(ior(240, ishft(cp, -18)))
620 utf8(2:2) = char(ior(128, iand(ishft(cp, -12), 63)))
621 utf8(3:3) = char(ior(128, iand(ishft(cp, -6), 63)))
622 utf8(4:4) = char(ior(128, iand(cp, 63)))
623 length = 4
624 else
625 ! Invalid codepoint
626 length = 0
627 end if
628 end subroutine codepoint_to_utf8
629
630 ! Callback: handle GLFW errors
631 subroutine error_callback(error_code, description) bind(C)
632 integer(c_int), value :: error_code
633 type(c_ptr), value :: description
634
635 print *, "GLFW Error ", error_code
636 end subroutine error_callback
637
638 ! Callback: handle mouse scroll wheel
639 subroutine scroll_callback(window, xoffset, yoffset) bind(C)
640 type(c_ptr), value :: window
641 real(c_double), value :: xoffset, yoffset
642 integer :: scroll_lines
643
644 if (.not. associated(active_term)) return
645
646 ! Convert scroll amount to lines (typically 3 lines per notch)
647 scroll_lines = nint(yoffset * 3.0d0)
648
649 ! Positive yoffset = scroll up (back in history)
650 call terminal_scroll_view(active_term, scroll_lines)
651 end subroutine scroll_callback
652
653 ! Set window title
654 subroutine window_set_title(win, title)
655 type(window_t), intent(in) :: win
656 character(len=*), intent(in) :: title
657 character(len=257) :: c_title
658
659 if (.not. c_associated(win%handle)) return
660
661 c_title = trim(title) // c_null_char
662 call glfwSetWindowTitle(win%handle, c_title)
663 end subroutine window_set_title
664
665 ! Enable background blur (macOS only, no-op on other platforms)
666 subroutine window_set_blur(win, enable)
667 type(window_t), intent(in) :: win
668 logical, intent(in) :: enable
669
670 if (.not. c_associated(win%handle)) return
671
672 if (enable) then
673 call fortty_set_window_blur_c(win%handle, 1_c_int)
674 else
675 call fortty_set_window_blur_c(win%handle, 0_c_int)
676 end if
677 end subroutine window_set_blur
678
679 ! Set cell dimensions for mouse coordinate conversion
680 subroutine window_set_cell_size(w, h)
681 integer, intent(in) :: w, h
682
683 cell_width = w
684 cell_height = h
685 end subroutine window_set_cell_size
686
687 ! Get selection state (for rendering)
688 function window_get_selection() result(sel)
689 type(selection_t) :: sel
690
691 sel = active_selection
692 end function window_get_selection
693
694 ! Set clipboard content
695 subroutine window_clipboard_set(win, text)
696 type(window_t), intent(in) :: win
697 character(len=*), intent(in) :: text
698 character(len=4097) :: c_text
699
700 if (.not. c_associated(win%handle)) return
701 if (len_trim(text) == 0) return
702
703 c_text = trim(text) // c_null_char
704 call glfwSetClipboardString(win%handle, c_text)
705 end subroutine window_clipboard_set
706
707 ! Get clipboard content
708 function window_clipboard_get(win) result(text)
709 type(window_t), intent(in) :: win
710 character(len=4096) :: text
711 type(c_ptr) :: clip_ptr
712 character(len=1), pointer :: chars(:)
713 integer :: i, length
714
715 text = ''
716 if (.not. c_associated(win%handle)) return
717
718 clip_ptr = glfwGetClipboardString(win%handle)
719 if (.not. c_associated(clip_ptr)) return
720
721 ! Convert C string to Fortran string
722 call c_f_pointer(clip_ptr, chars, [4096])
723 length = 0
724 do i = 1, 4096
725 if (chars(i) == c_null_char) exit
726 length = i
727 end do
728
729 do i = 1, min(length, 4096)
730 text(i:i) = chars(i)
731 end do
732 end function window_clipboard_get
733
734 ! Callback: handle mouse button events
735 subroutine mouse_button_callback(window, button, action, mods) bind(C)
736 type(c_ptr), value :: window
737 integer(c_int), value :: button, action, mods
738 real(c_double) :: xpos, ypos
739 integer :: col, row
740
741 ! Only handle left mouse button for selection
742 if (button /= GLFW_MOUSE_BUTTON_LEFT) return
743
744 call glfwGetCursorPos(window, xpos, ypos)
745
746 ! Convert pixel position to terminal cell coordinates (1-based)
747 col = int(xpos / cell_width) + 1
748 row = int(ypos / cell_height) + 1
749
750 ! Clamp to valid range
751 if (associated(active_term)) then
752 if (col < 1) col = 1
753 if (col > active_term%cols) col = active_term%cols
754 if (row < 1) row = 1
755 if (row > active_term%rows) row = active_term%rows
756 end if
757
758 if (action == GLFW_PRESS) then
759 call selection_start(active_selection, row, col)
760 else if (action == GLFW_RELEASE) then
761 call selection_end(active_selection)
762 ! Copy selection to clipboard if active
763 if (selection_is_active(active_selection)) then
764 call copy_selection_to_clipboard(window)
765 end if
766 end if
767 end subroutine mouse_button_callback
768
769 ! Callback: handle cursor position changes
770 subroutine cursor_pos_callback(window, xpos, ypos) bind(C)
771 type(c_ptr), value :: window
772 real(c_double), value :: xpos, ypos
773 integer :: col, row
774
775 ! Only update if actively selecting
776 if (.not. active_selection%selecting) return
777
778 ! Convert pixel position to terminal cell coordinates (1-based)
779 col = int(xpos / cell_width) + 1
780 row = int(ypos / cell_height) + 1
781
782 ! Clamp to valid range
783 if (associated(active_term)) then
784 if (col < 1) col = 1
785 if (col > active_term%cols) col = active_term%cols
786 if (row < 1) row = 1
787 if (row > active_term%rows) row = active_term%rows
788 end if
789
790 call selection_update(active_selection, row, col)
791 end subroutine cursor_pos_callback
792
793 ! Copy selection to clipboard
794 subroutine copy_selection_to_clipboard(window)
795 use screen_mod
796 use cell_mod
797 type(c_ptr), intent(in) :: window
798 type(screen_t), pointer :: scr
799 type(cell_t) :: cell
800 character(len=4096) :: text
801 character(len=4) :: utf8
802 integer :: r1, c1, r2, c2, row, col, pos, utf8_len
803
804 if (.not. associated(active_term)) return
805 if (.not. selection_is_active(active_selection)) return
806
807 scr => terminal_active_screen(active_term)
808 call selection_get_bounds(active_selection, r1, c1, r2, c2)
809
810 text = ''
811 pos = 1
812
813 do row = r1, r2
814 do col = 1, scr%cols
815 ! Check if this cell is in selection
816 if (.not. selection_contains(active_selection, row, col)) cycle
817
818 cell = screen_get_cell(scr, row, col)
819
820 ! Convert codepoint to UTF-8
821 if (cell%codepoint >= 32 .and. cell%codepoint < 1114112) then
822 call codepoint_to_utf8(cell%codepoint, utf8, utf8_len)
823 if (pos + utf8_len - 1 <= 4096) then
824 text(pos:pos+utf8_len-1) = utf8(1:utf8_len)
825 pos = pos + utf8_len
826 end if
827 end if
828 end do
829
830 ! Add newline between rows (except last row)
831 if (row < r2 .and. pos < 4096) then
832 text(pos:pos) = char(10)
833 pos = pos + 1
834 end if
835 end do
836
837 ! Set clipboard
838 if (pos > 1) then
839 call glfwSetClipboardString(window, trim(text(1:pos-1)) // c_null_char)
840 end if
841 end subroutine copy_selection_to_clipboard
842
843 ! Handle paste from clipboard
844 subroutine handle_paste(window)
845 type(c_ptr), intent(in) :: window
846 type(c_ptr) :: clip_ptr
847 character(len=1), pointer :: chars(:)
848 integer :: i, length
849
850 if (.not. associated(active_pty)) return
851
852 clip_ptr = glfwGetClipboardString(window)
853 if (.not. c_associated(clip_ptr)) return
854
855 ! Find length of C string
856 call c_f_pointer(clip_ptr, chars, [4096])
857 length = 0
858 do i = 1, 4096
859 if (chars(i) == c_null_char) exit
860 length = i
861 end do
862
863 ! Write to PTY
864 if (length > 0) then
865 block
866 character(len=4096) :: paste_text
867 integer :: j
868 paste_text = ''
869 do j = 1, length
870 paste_text(j:j) = chars(j)
871 end do
872 call pty_write(active_pty, paste_text, length)
873 end block
874 end if
875 end subroutine handle_paste
876
877 ! Get pending font size delta (returns 0 if none)
878 function window_get_font_delta() result(delta)
879 integer :: delta
880 delta = pending_font_delta
881 end function window_get_font_delta
882
883 ! Clear pending font size delta
884 subroutine window_clear_font_delta()
885 pending_font_delta = 0
886 end subroutine window_clear_font_delta
887
888 ! Set current font size (for tracking)
889 subroutine window_set_font_size(size)
890 integer, intent(in) :: size
891 current_font_size = size
892 end subroutine window_set_font_size
893
894 ! Set the render callback for live resize support
895 subroutine window_set_render_callback(callback)
896 procedure(render_callback_interface) :: callback
897
898 render_callback => callback
899 end subroutine window_set_render_callback
900
901 ! Check if window is currently resizing (for main loop optimization)
902 function window_is_resizing() result(resizing)
903 logical :: resizing
904 resizing = is_resizing
905 is_resizing = .false. ! Clear the flag after reading
906 end function window_is_resizing
907
908 ! Get pending tab action (returns 0 if none)
909 function window_get_tab_action() result(action)
910 integer :: action
911 action = pending_tab_action
912 end function window_get_tab_action
913
914 ! Clear pending tab action
915 subroutine window_clear_tab_action()
916 pending_tab_action = TAB_ACTION_NONE
917 end subroutine window_clear_tab_action
918
919 ! Get pending pane action (returns 0 if none)
920 function window_get_pane_action() result(action)
921 integer :: action
922 action = pending_pane_action
923 end function window_get_pane_action
924
925 ! Clear pending pane action
926 subroutine window_clear_pane_action()
927 pending_pane_action = PANE_ACTION_NONE
928 end subroutine window_clear_pane_action
929
930 end module window_mod
931