| 1 | ! Render state module - holds state for the render callback |
| 2 | ! This allows do_render to be a module procedure instead of an internal |
| 3 | ! procedure, avoiding the need for trampolines and executable stack. |
| 4 | module render_state_mod |
| 5 | use window_mod |
| 6 | use gl_bindings |
| 7 | use renderer_mod |
| 8 | use terminal_mod |
| 9 | use screen_mod |
| 10 | use cell_mod |
| 11 | use config_mod |
| 12 | use cursor_mod, only: CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR |
| 13 | use tab_manager_mod |
| 14 | use tab_bar_mod |
| 15 | use pane_mod |
| 16 | use selection_mod, only: selection_t, selection_contains |
| 17 | implicit none |
| 18 | private |
| 19 | |
| 20 | public :: render_state_init |
| 21 | public :: render_state_update_blink |
| 22 | public :: do_render |
| 23 | |
| 24 | ! Pointers to main program state |
| 25 | type(window_t), pointer, save :: rs_win => null() |
| 26 | type(renderer_t), pointer, save :: rs_ren => null() |
| 27 | type(tab_manager_t), pointer, save :: rs_tab_mgr => null() |
| 28 | type(config_t), pointer, save :: rs_cfg => null() |
| 29 | |
| 30 | ! Scalar state (copied, not pointed) |
| 31 | integer, save :: rs_prev_width = 0 |
| 32 | integer, save :: rs_prev_height = 0 |
| 33 | integer, save :: rs_win_width = 0 |
| 34 | integer, save :: rs_win_height = 0 |
| 35 | integer, save :: rs_cell_width = 10 |
| 36 | integer, save :: rs_cell_height = 18 |
| 37 | integer, save :: rs_ascender = 14 |
| 38 | logical, save :: rs_cursor_blink_visible = .true. |
| 39 | |
| 40 | ! Scrollback line buffer |
| 41 | type(cell_t), allocatable, save :: rs_sb_line(:) |
| 42 | |
| 43 | contains |
| 44 | |
| 45 | ! Initialize render state with pointers to main program data |
| 46 | subroutine render_state_init(win, ren, tab_mgr, cfg, & |
| 47 | cell_width, cell_height, ascender, & |
| 48 | win_width, win_height) |
| 49 | type(window_t), target, intent(in) :: win |
| 50 | type(renderer_t), target, intent(in) :: ren |
| 51 | type(tab_manager_t), target, intent(in) :: tab_mgr |
| 52 | type(config_t), target, intent(in) :: cfg |
| 53 | integer, intent(in) :: cell_width, cell_height, ascender |
| 54 | integer, intent(in) :: win_width, win_height |
| 55 | |
| 56 | rs_win => win |
| 57 | rs_ren => ren |
| 58 | rs_tab_mgr => tab_mgr |
| 59 | rs_cfg => cfg |
| 60 | rs_cell_width = cell_width |
| 61 | rs_cell_height = cell_height |
| 62 | rs_ascender = ascender |
| 63 | rs_win_width = win_width |
| 64 | rs_win_height = win_height |
| 65 | rs_prev_width = win_width |
| 66 | rs_prev_height = win_height |
| 67 | end subroutine render_state_init |
| 68 | |
| 69 | ! Update render state with current values |
| 70 | subroutine render_state_update_blink(cursor_blink_visible) |
| 71 | logical, intent(in) :: cursor_blink_visible |
| 72 | rs_cursor_blink_visible = cursor_blink_visible |
| 73 | end subroutine render_state_update_blink |
| 74 | |
| 75 | ! Main render subroutine - now a module procedure, not internal |
| 76 | subroutine do_render() |
| 77 | integer :: render_width, render_height |
| 78 | integer :: pane_idx, tab_idx, tab_bar_height |
| 79 | integer :: scissor_x, scissor_y, scissor_w, scissor_h |
| 80 | integer :: hover_tab |
| 81 | logical :: hover_close |
| 82 | real :: dim_factor, pane_x_offset, pane_y_offset |
| 83 | real :: bg_r, bg_g, bg_b |
| 84 | type(pane_t), pointer :: cur_pane |
| 85 | type(terminal_t), pointer :: pane_term |
| 86 | type(screen_t), pointer :: pane_scr |
| 87 | type(selection_t) :: sel |
| 88 | type(cell_t) :: cell |
| 89 | integer :: row, col, scroll_offset, sb_offset, screen_row |
| 90 | real :: x, y, r, g, b |
| 91 | |
| 92 | ! Guard against uninitialized state |
| 93 | if (.not. associated(rs_win)) return |
| 94 | if (.not. associated(rs_ren)) return |
| 95 | if (.not. associated(rs_tab_mgr)) return |
| 96 | if (.not. associated(rs_cfg)) return |
| 97 | |
| 98 | ! Get current window size and update projection if needed |
| 99 | call window_get_size(rs_win, render_width, render_height) |
| 100 | if (render_width /= rs_prev_width .or. render_height /= rs_prev_height) then |
| 101 | rs_prev_width = render_width |
| 102 | rs_prev_height = render_height |
| 103 | rs_win_width = render_width |
| 104 | rs_win_height = render_height |
| 105 | call renderer_set_projection(rs_ren, rs_win_width, rs_win_height) |
| 106 | end if |
| 107 | |
| 108 | ! Clear screen with background color and opacity from config |
| 109 | bg_r = real(rs_cfg%bg_color%r) / 255.0 |
| 110 | bg_g = real(rs_cfg%bg_color%g) / 255.0 |
| 111 | bg_b = real(rs_cfg%bg_color%b) / 255.0 |
| 112 | call glClearColor(bg_r, bg_g, bg_b, rs_cfg%window_opacity) |
| 113 | call glClear(GL_COLOR_BUFFER_BIT) |
| 114 | |
| 115 | ! Render terminal buffer |
| 116 | call renderer_begin(rs_ren) |
| 117 | |
| 118 | ! Get tab hover state for close button highlighting |
| 119 | call window_get_tab_hover(hover_tab, hover_close) |
| 120 | |
| 121 | ! Render tab bar at top |
| 122 | call tab_bar_render(rs_ren, rs_tab_mgr, rs_win_width, rs_tab_mgr%bar_height, & |
| 123 | rs_cell_width, rs_ascender, hover_tab, hover_close) |
| 124 | |
| 125 | ! Calculate effective tab bar height (hidden when only 1 tab) |
| 126 | if (rs_tab_mgr%count > 1) then |
| 127 | tab_bar_height = rs_tab_mgr%bar_height |
| 128 | else |
| 129 | tab_bar_height = 0 |
| 130 | end if |
| 131 | |
| 132 | ! Update window module with tab bar info for click detection |
| 133 | call window_set_tab_bar_info(tab_bar_height, rs_tab_mgr%count, rs_win_width) |
| 134 | |
| 135 | ! Guard against no active tab |
| 136 | if (rs_tab_mgr%active_index < 1 .or. rs_tab_mgr%active_index > rs_tab_mgr%count) then |
| 137 | call renderer_flush(rs_ren) |
| 138 | call window_swap_buffers(rs_win) |
| 139 | return |
| 140 | end if |
| 141 | |
| 142 | tab_idx = rs_tab_mgr%active_index |
| 143 | |
| 144 | ! Get current selection for highlighting (only applies to active pane) |
| 145 | sel = window_get_selection() |
| 146 | |
| 147 | ! Render each pane in the active tab |
| 148 | do pane_idx = 1, rs_tab_mgr%tabs(tab_idx)%pane_count |
| 149 | cur_pane => rs_tab_mgr%tabs(tab_idx)%panes(pane_idx) |
| 150 | pane_term => cur_pane%term |
| 151 | pane_scr => terminal_active_screen(pane_term) |
| 152 | |
| 153 | ! Determine dimming factor (inactive panes are dimmed) |
| 154 | if (cur_pane%active) then |
| 155 | dim_factor = 1.0 |
| 156 | else |
| 157 | dim_factor = 0.6 |
| 158 | end if |
| 159 | |
| 160 | ! Calculate pane offset for rendering |
| 161 | pane_x_offset = real(cur_pane%x) |
| 162 | pane_y_offset = real(cur_pane%y) |
| 163 | |
| 164 | ! Enable scissor test for this pane's viewport |
| 165 | ! OpenGL uses bottom-left origin, so convert from top-left |
| 166 | scissor_x = cur_pane%x |
| 167 | scissor_y = rs_win_height - cur_pane%y - cur_pane%height |
| 168 | scissor_w = cur_pane%width |
| 169 | scissor_h = cur_pane%height |
| 170 | call glEnable(GL_SCISSOR_TEST) |
| 171 | call glScissor(scissor_x, scissor_y, scissor_w, scissor_h) |
| 172 | |
| 173 | scroll_offset = terminal_get_scroll_offset(pane_term) |
| 174 | |
| 175 | ! Allocate/resize scrollback line buffer if needed |
| 176 | if (.not. allocated(rs_sb_line)) then |
| 177 | allocate(rs_sb_line(cur_pane%cols)) |
| 178 | else if (size(rs_sb_line) /= cur_pane%cols) then |
| 179 | deallocate(rs_sb_line) |
| 180 | allocate(rs_sb_line(cur_pane%cols)) |
| 181 | end if |
| 182 | |
| 183 | do row = 1, pane_scr%rows |
| 184 | ! Cell top-left y coordinate relative to pane |
| 185 | y = real(row - 1) * rs_cell_height + pane_y_offset |
| 186 | |
| 187 | ! Determine if this row shows scrollback or screen content |
| 188 | sb_offset = scroll_offset - row + 1 |
| 189 | |
| 190 | if (sb_offset > 0 .and. sb_offset <= terminal_get_scrollback_count(pane_term)) then |
| 191 | ! This row shows scrollback content |
| 192 | call terminal_get_scrollback_line(pane_term, sb_offset - 1, rs_sb_line, cur_pane%cols) |
| 193 | do col = 1, min(cur_pane%cols, pane_scr%cols) |
| 194 | cell = rs_sb_line(col) |
| 195 | |
| 196 | ! Skip continuation cells (2nd half of wide chars) |
| 197 | if (cell%is_continuation) cycle |
| 198 | |
| 199 | x = real(col - 1) * rs_cell_width + pane_x_offset |
| 200 | |
| 201 | ! Draw selection background if selected (only for active pane) |
| 202 | if (cur_pane%active .and. selection_contains(sel, row, col)) then |
| 203 | call renderer_draw_rect(rs_ren, x, y, real(rs_cell_width), real(rs_cell_height), & |
| 204 | 0.3, 0.3, 0.6, 1.0) |
| 205 | end if |
| 206 | |
| 207 | ! Only render cells with actual text content |
| 208 | if (cell%codepoint /= 32 .and. cell%codepoint /= 0) then |
| 209 | r = real(cell%fg%r) / 255.0 * dim_factor |
| 210 | g = real(cell%fg%g) / 255.0 * dim_factor |
| 211 | b = real(cell%fg%b) / 255.0 * dim_factor |
| 212 | call renderer_draw_char(rs_ren, x, y + real(rs_ascender), cell%codepoint, r, g, b, 1.0) |
| 213 | end if |
| 214 | end do |
| 215 | else |
| 216 | ! This row shows screen content |
| 217 | screen_row = row - scroll_offset |
| 218 | if (screen_row >= 1 .and. screen_row <= pane_scr%rows) then |
| 219 | do col = 1, pane_scr%cols |
| 220 | cell = screen_get_cell(pane_scr, screen_row, col) |
| 221 | |
| 222 | ! Skip continuation cells (2nd half of wide chars) |
| 223 | if (cell%is_continuation) cycle |
| 224 | |
| 225 | x = real(col - 1) * rs_cell_width + pane_x_offset |
| 226 | |
| 227 | ! Draw selection background if selected (only for active pane) |
| 228 | if (cur_pane%active .and. selection_contains(sel, row, col)) then |
| 229 | call renderer_draw_rect(rs_ren, x, y, real(rs_cell_width), real(rs_cell_height), & |
| 230 | 0.3, 0.3, 0.6, 1.0) |
| 231 | end if |
| 232 | |
| 233 | ! Only render cells with actual text content |
| 234 | if (cell%codepoint /= 32 .and. cell%codepoint /= 0) then |
| 235 | r = real(cell%fg%r) / 255.0 * dim_factor |
| 236 | g = real(cell%fg%g) / 255.0 * dim_factor |
| 237 | b = real(cell%fg%b) / 255.0 * dim_factor |
| 238 | call renderer_draw_char(rs_ren, x, y + real(rs_ascender), cell%codepoint, r, g, b, 1.0) |
| 239 | end if |
| 240 | end do |
| 241 | end if |
| 242 | end if |
| 243 | end do |
| 244 | |
| 245 | ! Draw cursor if visible (only for active pane and when not scrolled back) |
| 246 | if (cur_pane%active .and. pane_term%cursor%visible .and. scroll_offset == 0) then |
| 247 | ! Check blink state - only hide cursor if blink is enabled and in off phase |
| 248 | if (.not. pane_term%cursor%blink .or. rs_cursor_blink_visible) then |
| 249 | x = real(pane_term%cursor%col - 1) * rs_cell_width + pane_x_offset |
| 250 | y = real(pane_term%cursor%row - 1) * rs_cell_height + pane_y_offset |
| 251 | |
| 252 | select case (pane_term%cursor%style) |
| 253 | case (CURSOR_BLOCK) |
| 254 | call renderer_draw_rect(rs_ren, x, y, real(rs_cell_width), real(rs_cell_height), & |
| 255 | 0.7, 0.7, 0.7, 0.8) |
| 256 | case (CURSOR_UNDERLINE) |
| 257 | call renderer_draw_rect(rs_ren, x, y + real(rs_ascender), & |
| 258 | real(rs_cell_width), 2.0, 0.7, 0.7, 0.7, 1.0) |
| 259 | case (CURSOR_BAR) |
| 260 | call renderer_draw_rect(rs_ren, x, y, 2.0, real(rs_cell_height), 0.7, 0.7, 0.7, 1.0) |
| 261 | case default |
| 262 | call renderer_draw_rect(rs_ren, x, y, real(rs_cell_width), real(rs_cell_height), & |
| 263 | 0.7, 0.7, 0.7, 0.8) |
| 264 | end select |
| 265 | end if |
| 266 | end if |
| 267 | |
| 268 | call glDisable(GL_SCISSOR_TEST) |
| 269 | end do |
| 270 | |
| 271 | call renderer_flush(rs_ren) |
| 272 | |
| 273 | ! Swap buffers |
| 274 | call window_swap_buffers(rs_win) |
| 275 | end subroutine do_render |
| 276 | |
| 277 | end module render_state_mod |
| 278 |