Fortran · 11169 bytes Raw Blame History
1 ! Tab Widget Module for Sniffly
2 ! Manages visual tab bar UI with tab buttons
3 module tab_widget
4 use, intrinsic :: iso_c_binding
5 use gtk, only: gtk_box_new, gtk_box_append, gtk_box_remove, &
6 GTK_ORIENTATION_HORIZONTAL, gtk_button_new, &
7 gtk_button_new_with_label, gtk_button_set_label, &
8 gtk_widget_set_size_request, g_signal_connect, &
9 gtk_widget_add_css_class, gtk_widget_remove_css_class, &
10 gtk_label_new, gtk_box_set_spacing, gtk_widget_set_hexpand, &
11 gtk_widget_set_halign, GTK_ALIGN_END, gtk_widget_get_first_child, &
12 gtk_widget_get_next_sibling
13 use tab_manager, only: tab_state, get_tab, num_tabs, active_tab_index, &
14 MAX_TABS, switch_to_tab, create_tab, close_tab
15 implicit none
16 private
17
18 public :: create_tab_bar, refresh_tab_bar, get_tab_bar_widget, update_tab_visual_states, &
19 register_tab_switch_callback
20
21 ! Tab bar container
22 type(c_ptr), save :: tab_bar_container = c_null_ptr
23
24 ! Track button pointers to determine which tab was clicked
25 type(c_ptr), dimension(MAX_TABS), save :: tab_buttons = c_null_ptr
26 type(c_ptr), dimension(MAX_TABS), save :: close_buttons = c_null_ptr
27
28 ! Tab click callback interface
29 abstract interface
30 subroutine tab_click_callback(tab_index)
31 integer, intent(in) :: tab_index
32 end subroutine tab_click_callback
33 end interface
34
35 ! Close tab callback interface
36 abstract interface
37 subroutine close_tab_callback(tab_index)
38 integer, intent(in) :: tab_index
39 end subroutine close_tab_callback
40 end interface
41
42 ! New tab callback interface
43 abstract interface
44 subroutine new_tab_callback()
45 end subroutine new_tab_callback
46 end interface
47
48 ! Tab switch callback interface (for UI updates)
49 abstract interface
50 subroutine tab_switch_callback()
51 end subroutine tab_switch_callback
52 end interface
53
54 ! Registered callbacks
55 procedure(tab_click_callback), pointer, save :: tab_click_cb => null()
56 procedure(close_tab_callback), pointer, save :: close_tab_cb => null()
57 procedure(new_tab_callback), pointer, save :: new_tab_cb => null()
58 procedure(tab_switch_callback), pointer, save :: tab_switch_cb => null()
59
60 contains
61
62 ! Create the tab bar (horizontal box with tab buttons)
63 function create_tab_bar() result(tab_bar)
64 type(c_ptr) :: tab_bar
65
66 ! Create horizontal box for tabs (aligned to right)
67 tab_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5_c_int)
68 call gtk_widget_set_halign(tab_bar, GTK_ALIGN_END)
69 call gtk_box_set_spacing(tab_bar, 5_c_int)
70
71 ! Store reference
72 tab_bar_container = tab_bar
73
74 print *, "Tab bar container created"
75 end function create_tab_bar
76
77 ! Refresh tab bar (rebuild all tab buttons)
78 subroutine refresh_tab_bar()
79 type(c_ptr) :: plus_btn, tab_btn, close_btn, tab_container, child, next_child
80 type(tab_state), pointer :: tab
81 integer :: i
82 character(len=256) :: label_text
83
84 if (.not. c_associated(tab_bar_container)) then
85 print *, "ERROR: Tab bar not initialized"
86 return
87 end if
88
89 print *, "Refreshing tab bar with ", num_tabs, " tabs"
90
91 ! Clear all existing children from tab bar
92 child = gtk_widget_get_first_child(tab_bar_container)
93 do while (c_associated(child))
94 ! Get next sibling BEFORE removing current child
95 next_child = gtk_widget_get_next_sibling(child)
96 call gtk_box_remove(tab_bar_container, child)
97 child = next_child
98 end do
99
100 ! Clear button pointer arrays
101 tab_buttons(:) = c_null_ptr
102 close_buttons(:) = c_null_ptr
103
104 print *, "Cleared old tab bar widgets"
105
106 ! Create plus button first (leftmost)
107 plus_btn = gtk_button_new_with_label("+"//c_null_char)
108 call gtk_widget_set_size_request(plus_btn, 30_c_int, 28_c_int)
109 call g_signal_connect(plus_btn, "clicked"//c_null_char, &
110 c_funloc(on_plus_clicked), c_null_ptr)
111 call gtk_box_append(tab_bar_container, plus_btn)
112
113 ! Create tab buttons (one for each tab)
114 do i = 1, num_tabs
115 tab => get_tab(i)
116 if (.not. associated(tab)) cycle
117
118 ! Create horizontal container for this tab (label + close button)
119 tab_container = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2_c_int)
120
121 ! Format label - show just "Empty" for empty tabs, ".../basename" for normal tabs
122 if (trim(tab%label) == "Empty") then
123 label_text = "Empty"
124 else
125 label_text = ".../" // trim(tab%label)
126 end if
127
128 ! Create tab label button
129 tab_btn = gtk_button_new_with_label(trim(label_text)//c_null_char)
130 call gtk_widget_set_size_request(tab_btn, 110_c_int, 28_c_int)
131
132 ! Store button pointer so we can identify which tab was clicked
133 tab_buttons(i) = tab_btn
134
135 ! Add yellow border if active tab
136 if (i == active_tab_index) then
137 call gtk_widget_add_css_class(tab_btn, "active-tab"//c_null_char)
138 end if
139
140 ! Connect click handler for tab selection
141 call g_signal_connect(tab_btn, "clicked"//c_null_char, &
142 c_funloc(on_tab_clicked), c_null_ptr)
143
144 call gtk_box_append(tab_container, tab_btn)
145
146 ! Create close button (small × button)
147 close_btn = gtk_button_new_with_label("×"//c_null_char)
148 call gtk_widget_set_size_request(close_btn, 24_c_int, 28_c_int)
149
150 ! Store close button pointer
151 close_buttons(i) = close_btn
152
153 ! Connect click handler for closing tab
154 call g_signal_connect(close_btn, "clicked"//c_null_char, &
155 c_funloc(on_close_clicked), c_null_ptr)
156
157 call gtk_box_append(tab_container, close_btn)
158
159 ! Add the tab container to the tab bar
160 call gtk_box_append(tab_bar_container, tab_container)
161 print *, "Added tab button ", i, ": ", trim(label_text)
162 end do
163
164 print *, "Tab bar refreshed"
165 end subroutine refresh_tab_bar
166
167 ! Get the tab bar widget pointer
168 function get_tab_bar_widget() result(widget)
169 type(c_ptr) :: widget
170 widget = tab_bar_container
171 end function get_tab_bar_widget
172
173 ! Update visual states of tab buttons (yellow border for active tab)
174 subroutine update_tab_visual_states()
175 integer :: i
176
177 ! Update CSS classes for all tab buttons
178 do i = 1, num_tabs
179 if (.not. c_associated(tab_buttons(i))) cycle
180
181 ! Remove or add active-tab class based on whether this is the active tab
182 if (i == active_tab_index) then
183 call gtk_widget_add_css_class(tab_buttons(i), "active-tab"//c_null_char)
184 else
185 call gtk_widget_remove_css_class(tab_buttons(i), "active-tab"//c_null_char)
186 end if
187 end do
188
189 print *, "Updated tab visual states - active tab: ", active_tab_index
190 end subroutine update_tab_visual_states
191
192 ! Callback when plus button is clicked
193 subroutine on_plus_clicked(button, user_data) bind(c)
194 use iso_fortran_env, only: output_unit
195 type(c_ptr), value :: button, user_data
196 integer :: new_tab_index
197 type(tab_state), pointer :: new_tab, current_tab
198 character(len=512) :: new_tab_path
199
200 print *, "Plus button clicked - creating new tab"
201
202 ! New tabs start completely empty - no path to avoid accidental scans
203 new_tab_path = ""
204
205 ! Create a new empty tab
206 new_tab_index = create_tab(new_tab_path)
207
208 if (new_tab_index < 0) then
209 print *, "ERROR: Failed to create new tab (max tabs reached?)"
210 return
211 end if
212
213 print *, "Created new tab ", new_tab_index
214
215 ! Switch to the new tab
216 call switch_to_tab(new_tab_index)
217
218 ! Rebuild tab bar to show the new tab
219 call refresh_tab_bar()
220
221 ! Update visual states (yellow border on new active tab)
222 call update_tab_visual_states()
223
224 ! Update UI for the new tab (but don't auto-scan empty tabs)
225 if (associated(tab_switch_cb)) then
226 call tab_switch_cb()
227 end if
228
229 print *, "Tab bar refreshed and switched to new tab ", new_tab_index
230 end subroutine on_plus_clicked
231
232 ! Callback when a tab button is clicked
233 subroutine on_tab_clicked(button, user_data) bind(c)
234 type(c_ptr), value :: button, user_data
235 integer :: i, clicked_tab_index
236
237 ! Find which tab was clicked by comparing button pointers
238 clicked_tab_index = -1
239 do i = 1, num_tabs
240 if (c_associated(tab_buttons(i), button)) then
241 clicked_tab_index = i
242 exit
243 end if
244 end do
245
246 if (clicked_tab_index == -1) then
247 print *, "WARNING: Could not determine which tab was clicked"
248 return
249 end if
250
251 print *, "Tab ", clicked_tab_index, " clicked"
252
253 ! If already on this tab, nothing to do
254 if (clicked_tab_index == active_tab_index) then
255 print *, "Already on tab ", clicked_tab_index
256 return
257 end if
258
259 ! Switch to the clicked tab
260 call switch_to_tab(clicked_tab_index)
261
262 ! Update tab visual states (yellow border)
263 call update_tab_visual_states()
264
265 ! Call registered UI update callback (if registered)
266 if (associated(tab_switch_cb)) then
267 call tab_switch_cb()
268 end if
269
270 print *, "Switched to tab ", clicked_tab_index
271 end subroutine on_tab_clicked
272
273 ! Callback when a close button is clicked
274 subroutine on_close_clicked(button, user_data) bind(c)
275 type(c_ptr), value :: button, user_data
276 integer :: i, clicked_tab_index
277
278 ! Find which close button was clicked
279 clicked_tab_index = -1
280 do i = 1, num_tabs
281 if (c_associated(close_buttons(i), button)) then
282 clicked_tab_index = i
283 exit
284 end if
285 end do
286
287 if (clicked_tab_index == -1) then
288 print *, "WARNING: Could not determine which close button was clicked"
289 return
290 end if
291
292 print *, "Close button clicked for tab ", clicked_tab_index
293
294 ! Prevent closing last tab
295 if (num_tabs <= 1) then
296 print *, "ERROR: Cannot close last tab"
297 return
298 end if
299
300 ! Close the tab
301 call close_tab(clicked_tab_index)
302
303 ! Rebuild tab bar
304 call refresh_tab_bar()
305
306 ! Update visual states
307 call update_tab_visual_states()
308
309 ! Update UI for the new active tab
310 if (associated(tab_switch_cb)) then
311 call tab_switch_cb()
312 end if
313
314 print *, "Tab ", clicked_tab_index, " closed - now ", num_tabs, " tabs remaining"
315 end subroutine on_close_clicked
316
317 ! Register tab click callback
318 subroutine register_tab_click_callback(callback)
319 procedure(tab_click_callback) :: callback
320 tab_click_cb => callback
321 end subroutine register_tab_click_callback
322
323 ! Register close tab callback
324 subroutine register_close_tab_callback(callback)
325 procedure(close_tab_callback) :: callback
326 close_tab_cb => callback
327 end subroutine register_close_tab_callback
328
329 ! Register new tab callback
330 subroutine register_new_tab_callback(callback)
331 procedure(new_tab_callback) :: callback
332 new_tab_cb => callback
333 end subroutine register_new_tab_callback
334
335 ! Register tab switch callback (for UI updates)
336 subroutine register_tab_switch_callback(callback)
337 procedure(tab_switch_callback) :: callback
338 tab_switch_cb => callback
339 print *, "Tab switch callback registered"
340 end subroutine register_tab_switch_callback
341
342 end module tab_widget
343