@@ -5,13 +5,16 @@ module window_mod |
| 5 | use gl_bindings | 5 | use gl_bindings |
| 6 | use pty_mod | 6 | use pty_mod |
| 7 | use terminal_mod | 7 | use terminal_mod |
| | 8 | + use selection_mod |
| 8 | implicit none | 9 | implicit none |
| 9 | private | 10 | private |
| 10 | | 11 | |
| 11 | - public :: window_t | 12 | + public :: window_t, selection_t |
| 12 | public :: window_create, window_destroy | 13 | public :: window_create, window_destroy |
| 13 | public :: window_should_close, window_swap_buffers, window_poll_events | 14 | public :: window_should_close, window_swap_buffers, window_poll_events |
| 14 | public :: window_get_size, window_set_pty, window_set_terminal | 15 | public :: window_get_size, window_set_pty, window_set_terminal |
| | 16 | + public :: window_set_title, window_set_cell_size |
| | 17 | + public :: window_get_selection, window_clipboard_set, window_clipboard_get |
| 15 | | 18 | |
| 16 | type :: window_t | 19 | type :: window_t |
| 17 | type(c_ptr) :: handle = c_null_ptr | 20 | type(c_ptr) :: handle = c_null_ptr |
@@ -30,6 +33,13 @@ module window_mod |
| 30 | ! Module-level terminal pointer for scrollback | 33 | ! Module-level terminal pointer for scrollback |
| 31 | type(terminal_t), pointer, save :: active_term => null() | 34 | type(terminal_t), pointer, save :: active_term => null() |
| 32 | | 35 | |
| | 36 | + ! Selection state for mouse-based text selection |
| | 37 | + type(selection_t), save :: active_selection |
| | 38 | + |
| | 39 | + ! Cell dimensions for mouse coordinate conversion |
| | 40 | + integer, save :: cell_width = 10 |
| | 41 | + integer, save :: cell_height = 18 |
| | 42 | + |
| 33 | ! Interface to C helper for loading OpenGL | 43 | ! Interface to C helper for loading OpenGL |
| 34 | interface | 44 | interface |
| 35 | integer(c_int) function fortty_load_gl() bind(C, name="fortty_load_gl") | 45 | integer(c_int) function fortty_load_gl() bind(C, name="fortty_load_gl") |
@@ -99,6 +109,8 @@ contains |
| 99 | dummy = glfwSetKeyCallback(win%handle, c_funloc(key_callback)) | 109 | dummy = glfwSetKeyCallback(win%handle, c_funloc(key_callback)) |
| 100 | dummy = glfwSetCharCallback(win%handle, c_funloc(char_callback)) | 110 | dummy = glfwSetCharCallback(win%handle, c_funloc(char_callback)) |
| 101 | dummy = glfwSetScrollCallback(win%handle, c_funloc(scroll_callback)) | 111 | dummy = glfwSetScrollCallback(win%handle, c_funloc(scroll_callback)) |
| | 112 | + dummy = glfwSetMouseButtonCallback(win%handle, c_funloc(mouse_button_callback)) |
| | 113 | + dummy = glfwSetCursorPosCallback(win%handle, c_funloc(cursor_pos_callback)) |
| 102 | | 114 | |
| 103 | end function window_create | 115 | end function window_create |
| 104 | | 116 | |
@@ -198,6 +210,14 @@ contains |
| 198 | call terminal_reset_scroll_view(active_term) | 210 | call terminal_reset_scroll_view(active_term) |
| 199 | end if | 211 | end if |
| 200 | | 212 | |
| | 213 | + ! Handle Ctrl+Shift+V for paste |
| | 214 | + if (iand(mods, GLFW_MOD_CONTROL) /= 0 .and. iand(mods, GLFW_MOD_SHIFT) /= 0) then |
| | 215 | + if (key == GLFW_KEY_V) then |
| | 216 | + call handle_paste(window) |
| | 217 | + return |
| | 218 | + end if |
| | 219 | + end if |
| | 220 | + |
| 201 | ! Handle Ctrl combinations (these don't trigger char_callback) | 221 | ! Handle Ctrl combinations (these don't trigger char_callback) |
| 202 | if (iand(mods, GLFW_MOD_CONTROL) /= 0) then | 222 | if (iand(mods, GLFW_MOD_CONTROL) /= 0) then |
| 203 | if (key >= GLFW_KEY_A .and. key <= GLFW_KEY_Z) then | 223 | if (key >= GLFW_KEY_A .and. key <= GLFW_KEY_Z) then |
@@ -445,4 +465,214 @@ contains |
| 445 | call terminal_scroll_view(active_term, scroll_lines) | 465 | call terminal_scroll_view(active_term, scroll_lines) |
| 446 | end subroutine scroll_callback | 466 | end subroutine scroll_callback |
| 447 | | 467 | |
| | 468 | + ! Set window title |
| | 469 | + subroutine window_set_title(win, title) |
| | 470 | + type(window_t), intent(in) :: win |
| | 471 | + character(len=*), intent(in) :: title |
| | 472 | + character(len=257) :: c_title |
| | 473 | + |
| | 474 | + if (.not. c_associated(win%handle)) return |
| | 475 | + |
| | 476 | + c_title = trim(title) // c_null_char |
| | 477 | + call glfwSetWindowTitle(win%handle, c_title) |
| | 478 | + end subroutine window_set_title |
| | 479 | + |
| | 480 | + ! Set cell dimensions for mouse coordinate conversion |
| | 481 | + subroutine window_set_cell_size(w, h) |
| | 482 | + integer, intent(in) :: w, h |
| | 483 | + |
| | 484 | + cell_width = w |
| | 485 | + cell_height = h |
| | 486 | + end subroutine window_set_cell_size |
| | 487 | + |
| | 488 | + ! Get selection state (for rendering) |
| | 489 | + function window_get_selection() result(sel) |
| | 490 | + type(selection_t) :: sel |
| | 491 | + |
| | 492 | + sel = active_selection |
| | 493 | + end function window_get_selection |
| | 494 | + |
| | 495 | + ! Set clipboard content |
| | 496 | + subroutine window_clipboard_set(win, text) |
| | 497 | + type(window_t), intent(in) :: win |
| | 498 | + character(len=*), intent(in) :: text |
| | 499 | + character(len=4097) :: c_text |
| | 500 | + |
| | 501 | + if (.not. c_associated(win%handle)) return |
| | 502 | + if (len_trim(text) == 0) return |
| | 503 | + |
| | 504 | + c_text = trim(text) // c_null_char |
| | 505 | + call glfwSetClipboardString(win%handle, c_text) |
| | 506 | + end subroutine window_clipboard_set |
| | 507 | + |
| | 508 | + ! Get clipboard content |
| | 509 | + function window_clipboard_get(win) result(text) |
| | 510 | + type(window_t), intent(in) :: win |
| | 511 | + character(len=4096) :: text |
| | 512 | + type(c_ptr) :: clip_ptr |
| | 513 | + character(len=1), pointer :: chars(:) |
| | 514 | + integer :: i, length |
| | 515 | + |
| | 516 | + text = '' |
| | 517 | + if (.not. c_associated(win%handle)) return |
| | 518 | + |
| | 519 | + clip_ptr = glfwGetClipboardString(win%handle) |
| | 520 | + if (.not. c_associated(clip_ptr)) return |
| | 521 | + |
| | 522 | + ! Convert C string to Fortran string |
| | 523 | + call c_f_pointer(clip_ptr, chars, [4096]) |
| | 524 | + length = 0 |
| | 525 | + do i = 1, 4096 |
| | 526 | + if (chars(i) == c_null_char) exit |
| | 527 | + length = i |
| | 528 | + end do |
| | 529 | + |
| | 530 | + do i = 1, min(length, 4096) |
| | 531 | + text(i:i) = chars(i) |
| | 532 | + end do |
| | 533 | + end function window_clipboard_get |
| | 534 | + |
| | 535 | + ! Callback: handle mouse button events |
| | 536 | + subroutine mouse_button_callback(window, button, action, mods) bind(C) |
| | 537 | + type(c_ptr), value :: window |
| | 538 | + integer(c_int), value :: button, action, mods |
| | 539 | + real(c_double) :: xpos, ypos |
| | 540 | + integer :: col, row |
| | 541 | + |
| | 542 | + ! Only handle left mouse button for selection |
| | 543 | + if (button /= GLFW_MOUSE_BUTTON_LEFT) return |
| | 544 | + |
| | 545 | + call glfwGetCursorPos(window, xpos, ypos) |
| | 546 | + |
| | 547 | + ! Convert pixel position to terminal cell coordinates (1-based) |
| | 548 | + col = int(xpos / cell_width) + 1 |
| | 549 | + row = int(ypos / cell_height) + 1 |
| | 550 | + |
| | 551 | + ! Clamp to valid range |
| | 552 | + if (associated(active_term)) then |
| | 553 | + if (col < 1) col = 1 |
| | 554 | + if (col > active_term%cols) col = active_term%cols |
| | 555 | + if (row < 1) row = 1 |
| | 556 | + if (row > active_term%rows) row = active_term%rows |
| | 557 | + end if |
| | 558 | + |
| | 559 | + if (action == GLFW_PRESS) then |
| | 560 | + call selection_start(active_selection, row, col) |
| | 561 | + else if (action == GLFW_RELEASE) then |
| | 562 | + call selection_end(active_selection) |
| | 563 | + ! Copy selection to clipboard if active |
| | 564 | + if (selection_is_active(active_selection)) then |
| | 565 | + call copy_selection_to_clipboard(window) |
| | 566 | + end if |
| | 567 | + end if |
| | 568 | + end subroutine mouse_button_callback |
| | 569 | + |
| | 570 | + ! Callback: handle cursor position changes |
| | 571 | + subroutine cursor_pos_callback(window, xpos, ypos) bind(C) |
| | 572 | + type(c_ptr), value :: window |
| | 573 | + real(c_double), value :: xpos, ypos |
| | 574 | + integer :: col, row |
| | 575 | + |
| | 576 | + ! Only update if actively selecting |
| | 577 | + if (.not. active_selection%selecting) return |
| | 578 | + |
| | 579 | + ! Convert pixel position to terminal cell coordinates (1-based) |
| | 580 | + col = int(xpos / cell_width) + 1 |
| | 581 | + row = int(ypos / cell_height) + 1 |
| | 582 | + |
| | 583 | + ! Clamp to valid range |
| | 584 | + if (associated(active_term)) then |
| | 585 | + if (col < 1) col = 1 |
| | 586 | + if (col > active_term%cols) col = active_term%cols |
| | 587 | + if (row < 1) row = 1 |
| | 588 | + if (row > active_term%rows) row = active_term%rows |
| | 589 | + end if |
| | 590 | + |
| | 591 | + call selection_update(active_selection, row, col) |
| | 592 | + end subroutine cursor_pos_callback |
| | 593 | + |
| | 594 | + ! Copy selection to clipboard |
| | 595 | + subroutine copy_selection_to_clipboard(window) |
| | 596 | + use screen_mod |
| | 597 | + use cell_mod |
| | 598 | + type(c_ptr), intent(in) :: window |
| | 599 | + type(screen_t), pointer :: scr |
| | 600 | + type(cell_t) :: cell |
| | 601 | + character(len=4096) :: text |
| | 602 | + character(len=4) :: utf8 |
| | 603 | + integer :: r1, c1, r2, c2, row, col, pos, utf8_len |
| | 604 | + |
| | 605 | + if (.not. associated(active_term)) return |
| | 606 | + if (.not. selection_is_active(active_selection)) return |
| | 607 | + |
| | 608 | + scr => terminal_active_screen(active_term) |
| | 609 | + call selection_get_bounds(active_selection, r1, c1, r2, c2) |
| | 610 | + |
| | 611 | + text = '' |
| | 612 | + pos = 1 |
| | 613 | + |
| | 614 | + do row = r1, r2 |
| | 615 | + do col = 1, scr%cols |
| | 616 | + ! Check if this cell is in selection |
| | 617 | + if (.not. selection_contains(active_selection, row, col)) cycle |
| | 618 | + |
| | 619 | + cell = screen_get_cell(scr, row, col) |
| | 620 | + |
| | 621 | + ! Convert codepoint to UTF-8 |
| | 622 | + if (cell%codepoint >= 32 .and. cell%codepoint < 1114112) then |
| | 623 | + call codepoint_to_utf8(cell%codepoint, utf8, utf8_len) |
| | 624 | + if (pos + utf8_len - 1 <= 4096) then |
| | 625 | + text(pos:pos+utf8_len-1) = utf8(1:utf8_len) |
| | 626 | + pos = pos + utf8_len |
| | 627 | + end if |
| | 628 | + end if |
| | 629 | + end do |
| | 630 | + |
| | 631 | + ! Add newline between rows (except last row) |
| | 632 | + if (row < r2 .and. pos < 4096) then |
| | 633 | + text(pos:pos) = char(10) |
| | 634 | + pos = pos + 1 |
| | 635 | + end if |
| | 636 | + end do |
| | 637 | + |
| | 638 | + ! Set clipboard |
| | 639 | + if (pos > 1) then |
| | 640 | + call glfwSetClipboardString(window, trim(text(1:pos-1)) // c_null_char) |
| | 641 | + end if |
| | 642 | + end subroutine copy_selection_to_clipboard |
| | 643 | + |
| | 644 | + ! Handle paste from clipboard |
| | 645 | + subroutine handle_paste(window) |
| | 646 | + type(c_ptr), intent(in) :: window |
| | 647 | + type(c_ptr) :: clip_ptr |
| | 648 | + character(len=1), pointer :: chars(:) |
| | 649 | + integer :: i, length |
| | 650 | + |
| | 651 | + if (.not. associated(active_pty)) return |
| | 652 | + |
| | 653 | + clip_ptr = glfwGetClipboardString(window) |
| | 654 | + if (.not. c_associated(clip_ptr)) return |
| | 655 | + |
| | 656 | + ! Find length of C string |
| | 657 | + call c_f_pointer(clip_ptr, chars, [4096]) |
| | 658 | + length = 0 |
| | 659 | + do i = 1, 4096 |
| | 660 | + if (chars(i) == c_null_char) exit |
| | 661 | + length = i |
| | 662 | + end do |
| | 663 | + |
| | 664 | + ! Write to PTY |
| | 665 | + if (length > 0) then |
| | 666 | + block |
| | 667 | + character(len=4096) :: paste_text |
| | 668 | + integer :: j |
| | 669 | + paste_text = '' |
| | 670 | + do j = 1, length |
| | 671 | + paste_text(j:j) = chars(j) |
| | 672 | + end do |
| | 673 | + call pty_write(active_pty, paste_text, length) |
| | 674 | + end block |
| | 675 | + end if |
| | 676 | + end subroutine handle_paste |
| | 677 | + |
| 448 | end module window_mod | 678 | end module window_mod |