Fortran · 18257 bytes Raw Blame History
1 module fgof_screen
2 use fgof_screen_types, only : &
3 screen_buffer, &
4 screen_cell, &
5 screen_damage, &
6 screen_diff, &
7 screen_size, &
8 screen_style
9 implicit none
10 private
11
12 public :: &
13 allocate_screen, &
14 clear_screen, &
15 clear_screen_buffer, &
16 clear_screen_cell, &
17 clear_screen_damage, &
18 clear_screen_diff, &
19 clear_screen_size, &
20 clear_screen_style, &
21 diff_screen, &
22 fill_screen, &
23 put_cell, &
24 put_glyph, &
25 render_cursor_ansi, &
26 render_screen_ansi, &
27 render_screen_diff_ansi, &
28 resize_screen, &
29 set_cursor, &
30 screen_buffer, &
31 screen_cell, &
32 screen_damage, &
33 screen_diff, &
34 screen_size, &
35 screen_style
36
37 contains
38
39 function clear_screen_style() result(style)
40 type(screen_style) :: style
41
42 style%fg = -1
43 style%bg = -1
44 style%bold = .false.
45 style%dim = .false.
46 style%italic = .false.
47 style%underline = .false.
48 style%inverse = .false.
49 end function clear_screen_style
50
51 function clear_screen_cell() result(cell)
52 type(screen_cell) :: cell
53
54 cell%glyph = " "
55 cell%style = clear_screen_style()
56 end function clear_screen_cell
57
58 function clear_screen_size() result(size_value)
59 type(screen_size) :: size_value
60
61 size_value%width = 0
62 size_value%height = 0
63 end function clear_screen_size
64
65 function clear_screen_damage() result(damage)
66 type(screen_damage) :: damage
67
68 damage%active = .false.
69 damage%row_first = 0
70 damage%row_last = 0
71 damage%col_first = 0
72 damage%col_last = 0
73 damage%changed_cells = 0
74 end function clear_screen_damage
75
76 function clear_screen_diff() result(diff)
77 type(screen_diff) :: diff
78
79 diff%changed = .false.
80 diff%size_changed = .false.
81 diff%cursor_changed = .false.
82 diff%cursor_visibility_changed = .false.
83 diff%previous_size = clear_screen_size()
84 diff%current_size = clear_screen_size()
85 diff%damage = clear_screen_damage()
86 end function clear_screen_diff
87
88 function clear_screen_buffer() result(buffer)
89 type(screen_buffer) :: buffer
90
91 buffer%size = clear_screen_size()
92 buffer%cursor_row = 1
93 buffer%cursor_col = 1
94 buffer%cursor_visible = .true.
95 end function clear_screen_buffer
96
97 function allocate_screen(width, height) result(buffer)
98 integer, intent(in) :: width
99 integer, intent(in) :: height
100 type(screen_buffer) :: buffer
101
102 buffer = clear_screen_buffer()
103 if (width <= 0 .or. height <= 0) return
104
105 buffer%size%width = width
106 buffer%size%height = height
107 allocate(buffer%cells(height, width))
108 call clear_screen(buffer)
109 call clamp_cursor(buffer)
110 end function allocate_screen
111
112 subroutine resize_screen(buffer, width, height)
113 type(screen_buffer), intent(inout) :: buffer
114 integer, intent(in) :: width
115 integer, intent(in) :: height
116 type(screen_cell), allocatable :: grown(:, :)
117 integer :: copy_height
118 integer :: copy_width
119
120 if (width <= 0 .or. height <= 0) then
121 if (allocated(buffer%cells)) deallocate(buffer%cells)
122 buffer%size = clear_screen_size()
123 call clamp_cursor(buffer)
124 return
125 end if
126
127 allocate(grown(height, width))
128 grown = clear_screen_cell()
129
130 if (allocated(buffer%cells)) then
131 copy_height = min(size(buffer%cells, 1), height)
132 copy_width = min(size(buffer%cells, 2), width)
133 if (copy_height > 0 .and. copy_width > 0) then
134 grown(:copy_height, :copy_width) = buffer%cells(:copy_height, :copy_width)
135 end if
136 deallocate(buffer%cells)
137 end if
138
139 call move_alloc(grown, buffer%cells)
140 buffer%size%width = width
141 buffer%size%height = height
142 call clamp_cursor(buffer)
143 end subroutine resize_screen
144
145 subroutine fill_screen(buffer, glyph, style)
146 type(screen_buffer), intent(inout) :: buffer
147 character(len=*), intent(in) :: glyph
148 type(screen_style), intent(in), optional :: style
149 type(screen_cell) :: fill_cell
150 integer :: row
151 integer :: col
152
153 if (.not. allocated(buffer%cells)) return
154
155 fill_cell = clear_screen_cell()
156 if (len(glyph) > 0) fill_cell%glyph = glyph(1:1)
157 if (present(style)) fill_cell%style = style
158
159 do row = 1, size(buffer%cells, 1)
160 do col = 1, size(buffer%cells, 2)
161 buffer%cells(row, col) = fill_cell
162 end do
163 end do
164 end subroutine fill_screen
165
166 subroutine clear_screen(buffer, style)
167 type(screen_buffer), intent(inout) :: buffer
168 type(screen_style), intent(in), optional :: style
169
170 call fill_screen(buffer, " ", style)
171 end subroutine clear_screen
172
173 subroutine put_cell(buffer, row, col, cell)
174 type(screen_buffer), intent(inout) :: buffer
175 integer, intent(in) :: row
176 integer, intent(in) :: col
177 type(screen_cell), intent(in) :: cell
178
179 if (.not. screen_index_in_bounds(buffer, row, col)) return
180 buffer%cells(row, col) = cell
181 end subroutine put_cell
182
183 subroutine put_glyph(buffer, row, col, glyph, style)
184 type(screen_buffer), intent(inout) :: buffer
185 integer, intent(in) :: row
186 integer, intent(in) :: col
187 character(len=*), intent(in) :: glyph
188 type(screen_style), intent(in), optional :: style
189 type(screen_cell) :: cell
190
191 if (.not. screen_index_in_bounds(buffer, row, col)) return
192
193 cell = buffer%cells(row, col)
194 if (len(glyph) > 0) cell%glyph = glyph(1:1)
195 if (present(style)) cell%style = style
196 call put_cell(buffer, row, col, cell)
197 end subroutine put_glyph
198
199 subroutine set_cursor(buffer, row, col)
200 type(screen_buffer), intent(inout) :: buffer
201 integer, intent(in) :: row
202 integer, intent(in) :: col
203
204 buffer%cursor_row = row
205 buffer%cursor_col = col
206 call clamp_cursor(buffer)
207 end subroutine set_cursor
208
209 function diff_screen(previous, current) result(diff)
210 type(screen_buffer), intent(in) :: previous
211 type(screen_buffer), intent(in) :: current
212 type(screen_diff) :: diff
213 integer :: max_rows
214 integer :: max_cols
215 integer :: row
216 integer :: col
217
218 diff = clear_screen_diff()
219 diff%previous_size = previous%size
220 diff%current_size = current%size
221 diff%size_changed = .not. screen_sizes_equal(previous%size, current%size)
222 diff%cursor_changed = previous%cursor_row /= current%cursor_row .or. &
223 previous%cursor_col /= current%cursor_col
224 diff%cursor_visibility_changed = previous%cursor_visible .neqv. current%cursor_visible
225
226 max_rows = max(previous%size%height, current%size%height)
227 max_cols = max(previous%size%width, current%size%width)
228
229 do row = 1, max_rows
230 do col = 1, max_cols
231 if (screen_cell_changed(previous, current, row, col)) then
232 call record_damage(diff%damage, row, col)
233 end if
234 end do
235 end do
236
237 diff%changed = diff%damage%active .or. diff%size_changed .or. &
238 diff%cursor_changed .or. diff%cursor_visibility_changed
239 end function diff_screen
240
241 function render_screen_ansi(buffer) result(output)
242 type(screen_buffer), intent(in) :: buffer
243 character(len=:), allocatable :: output
244 integer :: row
245
246 output = hide_cursor_ansi() // clear_screen_ansi()
247
248 if (allocated(buffer%cells)) then
249 do row = 1, size(buffer%cells, 1)
250 output = output // move_cursor_ansi(row, 1)
251 output = output // render_current_row_ansi(buffer, row, 1, size(buffer%cells, 2))
252 end do
253 end if
254
255 output = output // reset_style_ansi() // render_cursor_ansi(buffer)
256 end function render_screen_ansi
257
258 function render_screen_diff_ansi(previous, current) result(output)
259 type(screen_buffer), intent(in) :: previous
260 type(screen_buffer), intent(in) :: current
261 character(len=:), allocatable :: output
262 type(screen_diff) :: diff
263 integer :: row
264
265 diff = diff_screen(previous, current)
266 if (.not. diff%changed) then
267 output = ""
268 return
269 end if
270
271 output = ""
272 if (diff%damage%active) then
273 output = hide_cursor_ansi()
274 do row = diff%damage%row_first, diff%damage%row_last
275 output = output // move_cursor_ansi(row, diff%damage%col_first)
276 output = output // render_diff_row_ansi(previous, current, row, diff%damage%col_first, diff%damage%col_last)
277 end do
278 output = output // reset_style_ansi()
279 end if
280
281 output = output // render_cursor_ansi(current)
282 end function render_screen_diff_ansi
283
284 function render_cursor_ansi(buffer) result(output)
285 type(screen_buffer), intent(in) :: buffer
286 character(len=:), allocatable :: output
287
288 if (buffer%cursor_visible) then
289 output = move_cursor_ansi(buffer%cursor_row, buffer%cursor_col) // show_cursor_ansi()
290 else
291 output = hide_cursor_ansi()
292 end if
293 end function render_cursor_ansi
294
295 logical function screen_index_in_bounds(buffer, row, col) result(in_bounds)
296 type(screen_buffer), intent(in) :: buffer
297 integer, intent(in) :: row
298 integer, intent(in) :: col
299
300 in_bounds = allocated(buffer%cells)
301 if (.not. in_bounds) return
302
303 in_bounds = row >= 1 .and. row <= size(buffer%cells, 1) .and. &
304 col >= 1 .and. col <= size(buffer%cells, 2)
305 end function screen_index_in_bounds
306
307 subroutine clamp_cursor(buffer)
308 type(screen_buffer), intent(inout) :: buffer
309
310 if (.not. allocated(buffer%cells)) then
311 buffer%cursor_row = 1
312 buffer%cursor_col = 1
313 return
314 end if
315
316 buffer%cursor_row = max(1, min(buffer%cursor_row, size(buffer%cells, 1)))
317 buffer%cursor_col = max(1, min(buffer%cursor_col, size(buffer%cells, 2)))
318 end subroutine clamp_cursor
319
320 logical function screen_cell_changed(previous, current, row, col) result(changed)
321 type(screen_buffer), intent(in) :: previous
322 type(screen_buffer), intent(in) :: current
323 integer, intent(in) :: row
324 integer, intent(in) :: col
325
326 changed = screen_index_in_bounds(previous, row, col) .neqv. &
327 screen_index_in_bounds(current, row, col)
328 if (changed) return
329
330 changed = .not. screen_cells_equal(screen_cell_at(previous, row, col), &
331 screen_cell_at(current, row, col))
332 end function screen_cell_changed
333
334 function screen_cell_at(buffer, row, col) result(cell)
335 type(screen_buffer), intent(in) :: buffer
336 integer, intent(in) :: row
337 integer, intent(in) :: col
338 type(screen_cell) :: cell
339
340 cell = clear_screen_cell()
341 if (.not. screen_index_in_bounds(buffer, row, col)) return
342 cell = buffer%cells(row, col)
343 end function screen_cell_at
344
345 logical function screen_cells_equal(left, right) result(equal)
346 type(screen_cell), intent(in) :: left
347 type(screen_cell), intent(in) :: right
348
349 equal = left%glyph == right%glyph .and. screen_styles_equal(left%style, right%style)
350 end function screen_cells_equal
351
352 logical function screen_styles_equal(left, right) result(equal)
353 type(screen_style), intent(in) :: left
354 type(screen_style), intent(in) :: right
355
356 equal = left%fg == right%fg .and. &
357 left%bg == right%bg .and. &
358 left%bold .eqv. right%bold .and. &
359 left%dim .eqv. right%dim .and. &
360 left%italic .eqv. right%italic .and. &
361 left%underline .eqv. right%underline .and. &
362 left%inverse .eqv. right%inverse
363 end function screen_styles_equal
364
365 logical function screen_sizes_equal(left, right) result(equal)
366 type(screen_size), intent(in) :: left
367 type(screen_size), intent(in) :: right
368
369 equal = left%width == right%width .and. left%height == right%height
370 end function screen_sizes_equal
371
372 subroutine record_damage(damage, row, col)
373 type(screen_damage), intent(inout) :: damage
374 integer, intent(in) :: row
375 integer, intent(in) :: col
376
377 if (.not. damage%active) then
378 damage%active = .true.
379 damage%row_first = row
380 damage%row_last = row
381 damage%col_first = col
382 damage%col_last = col
383 else
384 damage%row_first = min(damage%row_first, row)
385 damage%row_last = max(damage%row_last, row)
386 damage%col_first = min(damage%col_first, col)
387 damage%col_last = max(damage%col_last, col)
388 end if
389
390 damage%changed_cells = damage%changed_cells + 1
391 end subroutine record_damage
392
393 function render_current_row_ansi(buffer, row, col_first, col_last) result(output)
394 type(screen_buffer), intent(in) :: buffer
395 integer, intent(in) :: row
396 integer, intent(in) :: col_first
397 integer, intent(in) :: col_last
398 character(len=:), allocatable :: output
399 type(screen_cell) :: cell
400 character(len=:), allocatable :: current_key
401 character(len=:), allocatable :: cell_key
402 integer :: col
403
404 output = ""
405 current_key = ""
406
407 do col = col_first, col_last
408 cell = buffer%cells(row, col)
409 cell_key = style_key(cell%style)
410 if (cell_key /= current_key) then
411 if (len(cell_key) > 0) then
412 output = output // style_ansi(cell%style)
413 else if (len(current_key) > 0) then
414 output = output // reset_style_ansi()
415 end if
416 current_key = cell_key
417 end if
418 output = output // renderable_glyph(cell%glyph)
419 end do
420
421 if (len(current_key) > 0) then
422 output = output // reset_style_ansi()
423 end if
424 end function render_current_row_ansi
425
426 function render_diff_row_ansi(previous, current, row, col_first, col_last) result(output)
427 type(screen_buffer), intent(in) :: previous
428 type(screen_buffer), intent(in) :: current
429 integer, intent(in) :: row
430 integer, intent(in) :: col_first
431 integer, intent(in) :: col_last
432 character(len=:), allocatable :: output
433 type(screen_cell) :: cell
434 character(len=:), allocatable :: current_key
435 character(len=:), allocatable :: cell_key
436 integer :: col
437
438 output = ""
439 current_key = ""
440
441 do col = col_first, col_last
442 cell = screen_cell_for_diff(previous, current, row, col)
443 cell_key = style_key(cell%style)
444 if (cell_key /= current_key) then
445 if (len(cell_key) > 0) then
446 output = output // style_ansi(cell%style)
447 else if (len(current_key) > 0) then
448 output = output // reset_style_ansi()
449 end if
450 current_key = cell_key
451 end if
452 output = output // renderable_glyph(cell%glyph)
453 end do
454
455 if (len(current_key) > 0) then
456 output = output // reset_style_ansi()
457 end if
458 end function render_diff_row_ansi
459
460 function screen_cell_for_diff(previous, current, row, col) result(cell)
461 type(screen_buffer), intent(in) :: previous
462 type(screen_buffer), intent(in) :: current
463 integer, intent(in) :: row
464 integer, intent(in) :: col
465 type(screen_cell) :: cell
466
467 cell = clear_screen_cell()
468 if (screen_index_in_bounds(current, row, col)) then
469 cell = current%cells(row, col)
470 return
471 end if
472
473 if (screen_index_in_bounds(previous, row, col)) then
474 cell = clear_screen_cell()
475 end if
476 end function screen_cell_for_diff
477
478 logical function style_is_default(style) result(is_default)
479 type(screen_style), intent(in) :: style
480
481 is_default = style%fg < 0 .and. style%bg < 0 .and. &
482 (.not. style%bold) .and. (.not. style%dim) .and. &
483 (.not. style%italic) .and. (.not. style%underline) .and. &
484 (.not. style%inverse)
485 end function style_is_default
486
487 function style_key(style) result(key)
488 type(screen_style), intent(in) :: style
489 character(len=:), allocatable :: key
490
491 if (style_is_default(style)) then
492 key = ""
493 return
494 end if
495
496 key = integer_text(style%fg) // ":" // integer_text(style%bg) // ":" // &
497 merge("1", "0", style%bold) // ":" // merge("1", "0", style%dim) // ":" // &
498 merge("1", "0", style%italic) // ":" // merge("1", "0", style%underline) // ":" // &
499 merge("1", "0", style%inverse)
500 end function style_key
501
502 function style_ansi(style) result(output)
503 type(screen_style), intent(in) :: style
504 character(len=:), allocatable :: output
505
506 output = reset_style_ansi()
507 if (style%bold) output = output // sgr_parameter("1")
508 if (style%dim) output = output // sgr_parameter("2")
509 if (style%italic) output = output // sgr_parameter("3")
510 if (style%underline) output = output // sgr_parameter("4")
511 if (style%inverse) output = output // sgr_parameter("7")
512 if (style%fg >= 0) output = output // sgr_parameter("38;5;" // integer_text(style%fg))
513 if (style%bg >= 0) output = output // sgr_parameter("48;5;" // integer_text(style%bg))
514 end function style_ansi
515
516 function sgr_parameter(parameter) result(output)
517 character(len=*), intent(in) :: parameter
518 character(len=:), allocatable :: output
519
520 output = csi() // parameter // "m"
521 end function sgr_parameter
522
523 function reset_style_ansi() result(output)
524 character(len=:), allocatable :: output
525
526 output = csi() // "0m"
527 end function reset_style_ansi
528
529 function clear_screen_ansi() result(output)
530 character(len=:), allocatable :: output
531
532 output = csi() // "2J" // csi() // "H"
533 end function clear_screen_ansi
534
535 function move_cursor_ansi(row, col) result(output)
536 integer, intent(in) :: row
537 integer, intent(in) :: col
538 character(len=:), allocatable :: output
539
540 output = csi() // integer_text(max(1, row)) // ";" // integer_text(max(1, col)) // "H"
541 end function move_cursor_ansi
542
543 function hide_cursor_ansi() result(output)
544 character(len=:), allocatable :: output
545
546 output = csi() // "?25l"
547 end function hide_cursor_ansi
548
549 function show_cursor_ansi() result(output)
550 character(len=:), allocatable :: output
551
552 output = csi() // "?25h"
553 end function show_cursor_ansi
554
555 function csi() result(output)
556 character(len=:), allocatable :: output
557
558 output = achar(27) // "["
559 end function csi
560
561 function integer_text(value) result(text)
562 integer, intent(in) :: value
563 character(len=:), allocatable :: text
564 character(len=32) :: scratch
565
566 write(scratch, "(i0)") value
567 text = trim(scratch)
568 end function integer_text
569
570 function renderable_glyph(glyph) result(output)
571 character(len=1), intent(in) :: glyph
572 character(len=1) :: output
573 integer :: code
574
575 code = iachar(glyph)
576 if (code < 32 .or. code == 127) then
577 output = "?"
578 else
579 output = glyph
580 end if
581 end function renderable_glyph
582
583 end module fgof_screen
584