fortrangoingonforty/fortty / a5be0dd

Browse files

Add hoverable close buttons to tab bar

Clicking the X button on a tab now closes that tab. Close buttons
highlight red on hover for visual feedback.
Authored by espadonne
SHA
a5be0dd33e0fa4b320e2f90c75e08923ae60b817
Parents
3fe456d
Tree
203c15b

4 changed files

StatusFile+-
M src/fortty.f90 6 0
M src/render_state.f90 6 1
M src/tabs/tab_bar.f90 54 3
M src/window/window.f90 51 5
src/fortty.f90modified
@@ -532,6 +532,12 @@ contains
532532
           if (target_tab <= tab_mgr%count) then
533533
             call tab_manager_switch(tab_mgr, target_tab)
534534
           end if
535
+        ! Check for close tab 1-9 (actions 20-28)
536
+        else if (action >= 20 .and. action <= 28) then
537
+          target_tab = action - 19  ! 20 -> close tab 1, 21 -> close tab 2, etc.
538
+          if (target_tab <= tab_mgr%count) then
539
+            call tab_manager_close(tab_mgr, target_tab)
540
+          end if
535541
         end if
536542
     end select
537543
 
src/render_state.f90modified
@@ -77,6 +77,8 @@ contains
7777
     integer :: render_width, render_height
7878
     integer :: pane_idx, tab_idx, tab_bar_height
7979
     integer :: scissor_x, scissor_y, scissor_w, scissor_h
80
+    integer :: hover_tab
81
+    logical :: hover_close
8082
     real :: dim_factor, pane_x_offset, pane_y_offset
8183
     real :: bg_r, bg_g, bg_b
8284
     type(pane_t), pointer :: cur_pane
@@ -113,9 +115,12 @@ contains
113115
     ! Render terminal buffer
114116
     call renderer_begin(rs_ren)
115117
 
118
+    ! Get tab hover state for close button highlighting
119
+    call window_get_tab_hover(hover_tab, hover_close)
120
+
116121
     ! Render tab bar at top
117122
     call tab_bar_render(rs_ren, rs_tab_mgr, rs_win_width, rs_tab_mgr%bar_height, &
118
-                        rs_cell_width, rs_ascender)
123
+                        rs_cell_width, rs_ascender, hover_tab, hover_close)
119124
 
120125
     ! Calculate effective tab bar height (hidden when only 1 tab)
121126
     if (rs_tab_mgr%count > 1) then
src/tabs/tab_bar.f90modified
@@ -5,19 +5,43 @@ module tab_bar_mod
55
   private
66
 
77
   public :: tab_bar_render
8
+  public :: tab_bar_get_close_button_bounds
9
+  public :: CLOSE_BTN_SIZE, CLOSE_BTN_MARGIN
810
 
911
   ! Tab bar styling constants
1012
   real, parameter :: TAB_PADDING = 8.0      ! Horizontal padding inside tab
1113
   real, parameter :: TAB_MIN_WIDTH = 80.0   ! Minimum tab width
1214
   real, parameter :: TAB_MAX_WIDTH = 200.0  ! Maximum tab width
15
+  real, parameter :: CLOSE_BTN_SIZE = 14.0  ! Close button size (square)
16
+  real, parameter :: CLOSE_BTN_MARGIN = 6.0 ! Margin from tab right edge
1317
 
1418
 contains
1519
 
20
+  ! Get close button bounds for a specific tab (for hit testing)
21
+  subroutine tab_bar_get_close_button_bounds(tab_index, tab_count, win_width, bar_height, &
22
+                                              btn_x, btn_y, btn_size)
23
+    integer, intent(in) :: tab_index, tab_count, win_width, bar_height
24
+    real, intent(out) :: btn_x, btn_y, btn_size
25
+    real :: tab_width, tab_start_x
26
+
27
+    tab_width = real(win_width) / real(tab_count)
28
+    if (tab_width > TAB_MAX_WIDTH) tab_width = TAB_MAX_WIDTH
29
+    if (tab_width < TAB_MIN_WIDTH) tab_width = TAB_MIN_WIDTH
30
+
31
+    tab_start_x = real(tab_index - 1) * tab_width
32
+    btn_size = CLOSE_BTN_SIZE
33
+    btn_x = tab_start_x + tab_width - CLOSE_BTN_MARGIN - CLOSE_BTN_SIZE
34
+    btn_y = (real(bar_height) - CLOSE_BTN_SIZE) / 2.0
35
+  end subroutine tab_bar_get_close_button_bounds
36
+
1637
   ! Render the tab bar at the top of the window
17
-  subroutine tab_bar_render(ren, mgr, win_width, bar_height, cell_width, ascender)
38
+  subroutine tab_bar_render(ren, mgr, win_width, bar_height, cell_width, ascender, &
39
+                            hover_tab, hover_close_btn)
1840
     type(renderer_t), intent(inout) :: ren
1941
     type(tab_manager_t), intent(in) :: mgr
2042
     integer, intent(in) :: win_width, bar_height, cell_width, ascender
43
+    integer, intent(in) :: hover_tab       ! Which tab mouse is over (0 = none)
44
+    logical, intent(in) :: hover_close_btn ! Is mouse over close button?
2145
     integer :: i, title_len
2246
     real :: x, y, tab_width
2347
     real :: bg_r, bg_g, bg_b
@@ -25,6 +49,8 @@ contains
2549
     real :: inactive_bg_r, inactive_bg_g, inactive_bg_b
2650
     real :: text_r, text_g, text_b
2751
     real :: divider_r, divider_g, divider_b
52
+    real :: close_btn_x, close_btn_y
53
+    real :: close_r, close_g, close_b
2854
     character(len=256) :: display_title
2955
     integer :: max_chars
3056
 
@@ -69,8 +95,8 @@ contains
6995
       tab_width = TAB_MIN_WIDTH
7096
     end if
7197
 
72
-    ! Maximum characters that fit in a tab (rough estimate)
73
-    max_chars = int((tab_width - 2.0 * TAB_PADDING) / real(cell_width))
98
+    ! Maximum characters that fit in a tab (account for close button)
99
+    max_chars = int((tab_width - 2.0 * TAB_PADDING - CLOSE_BTN_SIZE - CLOSE_BTN_MARGIN) / real(cell_width))
74100
     if (max_chars < 3) max_chars = 3
75101
 
76102
     x = 0.0
@@ -103,6 +129,31 @@ contains
103129
                                 real(bar_height) / 2.0 + real(ascender) / 2.0, &
104130
                                 trim(display_title), text_r, text_g, text_b, 1.0)
105131
 
132
+      ! Draw close button (X)
133
+      close_btn_x = x + tab_width - CLOSE_BTN_MARGIN - CLOSE_BTN_SIZE
134
+      close_btn_y = (real(bar_height) - CLOSE_BTN_SIZE) / 2.0
135
+
136
+      ! Close button color: red on hover, gray otherwise
137
+      if (i == hover_tab .and. hover_close_btn) then
138
+        ! Hovered - show red background circle and white X
139
+        call renderer_draw_rect(ren, close_btn_x, close_btn_y, &
140
+                                CLOSE_BTN_SIZE, CLOSE_BTN_SIZE, &
141
+                                0.8, 0.2, 0.2, 1.0)
142
+        close_r = 1.0
143
+        close_g = 1.0
144
+        close_b = 1.0
145
+      else
146
+        ! Not hovered - subtle gray X
147
+        close_r = 0.5
148
+        close_g = 0.5
149
+        close_b = 0.5
150
+      end if
151
+
152
+      ! Draw X character centered in the button
153
+      call renderer_draw_string(ren, close_btn_x + 2.0, &
154
+                                close_btn_y + real(ascender) - 2.0, &
155
+                                'x', close_r, close_g, close_b, 1.0)
156
+
106157
       ! Draw vertical divider after tab (except last)
107158
       if (i < mgr%count) then
108159
         call renderer_draw_rect(ren, x + tab_width - 1.0, 4.0, 1.0, real(bar_height - 8), &
src/window/window.f90modified
@@ -21,6 +21,7 @@ module window_mod
2121
   public :: window_get_pane_action, window_clear_pane_action
2222
   public :: window_is_focused
2323
   public :: window_set_tab_bar_info
24
+  public :: window_get_tab_hover
2425
 
2526
   ! Tab action constants
2627
   integer, parameter, public :: TAB_ACTION_NONE = 0
@@ -78,6 +79,10 @@ module window_mod
7879
   integer, save :: tab_count = 1            ! Number of tabs
7980
   integer, save :: tab_bar_win_width = 800  ! Window width for tab width calc
8081
 
82
+  ! Tab bar hover state
83
+  integer, save :: hover_tab_index = 0      ! Which tab mouse is over (0 = none)
84
+  logical, save :: hover_on_close_btn = .false.  ! Is mouse over close button?
85
+
8186
   ! Live resize rendering support
8287
   logical, save :: is_resizing = .false.
8388
   procedure(render_callback_interface), pointer, save :: render_callback => null()
@@ -777,7 +782,7 @@ contains
777782
     real(c_double) :: xpos, ypos
778783
     integer :: col, row
779784
     integer :: clicked_tab
780
-    real :: tab_width
785
+    real :: tab_width, btn_x, btn_y
781786
 
782787
     ! Unused argument (required by GLFW callback signature)
783788
     if (.false.) print *, mods
@@ -798,8 +803,18 @@ contains
798803
 
799804
         clicked_tab = int(xpos / tab_width) + 1
800805
         if (clicked_tab >= 1 .and. clicked_tab <= tab_count) then
801
-          ! Set pending action: 10 = tab 1, 11 = tab 2, etc.
802
-          pending_tab_action = 9 + clicked_tab
806
+          ! Check if clicking the close button (14x14 button, 6px from right edge)
807
+          btn_x = real(clicked_tab - 1) * tab_width + tab_width - 6.0 - 14.0
808
+          btn_y = (real(tab_bar_height) - 14.0) / 2.0
809
+
810
+          if (real(xpos) >= btn_x .and. real(xpos) <= btn_x + 14.0 .and. &
811
+              real(ypos) >= btn_y .and. real(ypos) <= btn_y + 14.0) then
812
+            ! Clicked close button - set action: 20 = close tab 1, 21 = close tab 2, etc.
813
+            pending_tab_action = 19 + clicked_tab
814
+          else
815
+            ! Clicked tab body - switch to tab: 10 = tab 1, 11 = tab 2, etc.
816
+            pending_tab_action = 9 + clicked_tab
817
+          end if
803818
         end if
804819
         return  ! Don't start text selection when clicking tabs
805820
       end if
@@ -834,16 +849,39 @@ contains
834849
     type(c_ptr), value :: window
835850
     real(c_double), value :: xpos, ypos
836851
     integer :: col, row
852
+    real :: tab_width, btn_x, btn_y, btn_size
837853
 
838854
     ! Suppress unused argument warning (required by GLFW callback signature)
839855
     if (.false. .and. c_associated(window)) continue
840856
 
841
-    ! Only update if actively selecting
857
+    ! Track tab bar hover state for close button highlighting
858
+    if (tab_bar_height > 0 .and. tab_count > 1 .and. int(ypos) < tab_bar_height) then
859
+      ! Mouse is in tab bar - calculate which tab
860
+      tab_width = real(tab_bar_win_width) / real(tab_count)
861
+      if (tab_width > 200.0) tab_width = 200.0
862
+      if (tab_width < 80.0) tab_width = 80.0
863
+
864
+      hover_tab_index = int(xpos / tab_width) + 1
865
+      if (hover_tab_index > tab_count) hover_tab_index = tab_count
866
+
867
+      ! Check if over close button (14x14 button, 6px from right edge)
868
+      btn_size = 14.0
869
+      btn_x = real(hover_tab_index - 1) * tab_width + tab_width - 6.0 - btn_size
870
+      btn_y = (real(tab_bar_height) - btn_size) / 2.0
871
+
872
+      hover_on_close_btn = (real(xpos) >= btn_x .and. real(xpos) <= btn_x + btn_size .and. &
873
+                           real(ypos) >= btn_y .and. real(ypos) <= btn_y + btn_size)
874
+    else
875
+      hover_tab_index = 0
876
+      hover_on_close_btn = .false.
877
+    end if
878
+
879
+    ! Only update selection if actively selecting
842880
     if (.not. active_selection%selecting) return
843881
 
844882
     ! Convert pixel position to terminal cell coordinates (1-based)
845883
     col = int(xpos / cell_width) + 1
846
-    row = int(ypos / cell_height) + 1
884
+    row = int((ypos - real(tab_bar_height)) / cell_height) + 1
847885
 
848886
     ! Clamp to valid range
849887
     if (associated(active_term)) then
@@ -1001,4 +1039,12 @@ contains
10011039
     tab_bar_win_width = win_width
10021040
   end subroutine window_set_tab_bar_info
10031041
 
1042
+  ! Get tab hover state for rendering close button highlights
1043
+  subroutine window_get_tab_hover(tab_idx, on_close_btn)
1044
+    integer, intent(out) :: tab_idx
1045
+    logical, intent(out) :: on_close_btn
1046
+    tab_idx = hover_tab_index
1047
+    on_close_btn = hover_on_close_btn
1048
+  end subroutine window_get_tab_hover
1049
+
10041050
 end module window_mod