@@ -5,13 +5,16 @@ module window_mod |
| 5 | 5 | use gl_bindings |
| 6 | 6 | use pty_mod |
| 7 | 7 | use terminal_mod |
| 8 | + use selection_mod |
| 8 | 9 | implicit none |
| 9 | 10 | private |
| 10 | 11 | |
| 11 | | - public :: window_t |
| 12 | + public :: window_t, selection_t |
| 12 | 13 | public :: window_create, window_destroy |
| 13 | 14 | public :: window_should_close, window_swap_buffers, window_poll_events |
| 14 | 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 | 19 | type :: window_t |
| 17 | 20 | type(c_ptr) :: handle = c_null_ptr |
@@ -30,6 +33,13 @@ module window_mod |
| 30 | 33 | ! Module-level terminal pointer for scrollback |
| 31 | 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 | 43 | ! Interface to C helper for loading OpenGL |
| 34 | 44 | interface |
| 35 | 45 | integer(c_int) function fortty_load_gl() bind(C, name="fortty_load_gl") |
@@ -99,6 +109,8 @@ contains |
| 99 | 109 | dummy = glfwSetKeyCallback(win%handle, c_funloc(key_callback)) |
| 100 | 110 | dummy = glfwSetCharCallback(win%handle, c_funloc(char_callback)) |
| 101 | 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 | 115 | end function window_create |
| 104 | 116 | |
@@ -198,6 +210,14 @@ contains |
| 198 | 210 | call terminal_reset_scroll_view(active_term) |
| 199 | 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 | 221 | ! Handle Ctrl combinations (these don't trigger char_callback) |
| 202 | 222 | if (iand(mods, GLFW_MOD_CONTROL) /= 0) then |
| 203 | 223 | if (key >= GLFW_KEY_A .and. key <= GLFW_KEY_Z) then |
@@ -445,4 +465,214 @@ contains |
| 445 | 465 | call terminal_scroll_view(active_term, scroll_lines) |
| 446 | 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 | 678 | end module window_mod |