v2 / vlib / term / ui / ui.c.v
282 lines · 253 sloc · 7.4 KB · 960264bdeb16ff056287d2869a388d3d5931a60c
Raw
1// Copyright (c) 2020-2024 Raúl Hernández. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module ui
5
6import strings
7
8pub struct Color {
9pub:
10 r u8
11 g u8
12 b u8
13}
14
15// hex returns `c`'s RGB color in hex format.
16pub fn (c Color) hex() string {
17 return '#${c.r.hex()}${c.g.hex()}${c.b.hex()}'
18}
19
20// Synchronized Updates spec, designed to avoid tearing during renders
21// https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
22const bsu = '\x1bP=1s\x1b\\'
23
24const esu = '\x1bP=2s\x1b\\'
25
26// write puts the string `s` into the print buffer.
27@[inline]
28pub fn (mut ctx Context) write(s string) {
29 if s == '' {
30 return
31 }
32 unsafe { ctx.print_buf.push_many(s.str, s.len) }
33}
34
35// flush displays the accumulated print buffer to the screen.
36@[inline]
37pub fn (mut ctx Context) flush() {
38 // TODO: Diff the previous frame against this one, and only render things that changed?
39 if !ctx.enable_su {
40 C.write(1, ctx.print_buf.data, ctx.print_buf.len)
41 } else {
42 C.write(1, bsu.str, bsu.len)
43 C.write(1, ctx.print_buf.data, ctx.print_buf.len)
44 C.write(1, esu.str, esu.len)
45 }
46 ctx.print_buf.clear()
47}
48
49// bold sets the character state to bold.
50@[inline]
51pub fn (mut ctx Context) bold() {
52 ctx.write('\x1b[1m')
53}
54
55// set_cursor_position positions the cusor at the given coordinates `x`,`y`.
56@[inline]
57pub fn (mut ctx Context) set_cursor_position(x int, y int) {
58 ctx.write('\x1b[${y};${x}H')
59}
60
61// show_cursor will make the cursor appear if it is not already visible
62@[inline]
63pub fn (mut ctx Context) show_cursor() {
64 ctx.write('\x1b[?25h')
65}
66
67// hide_cursor will make the cursor invisible
68@[inline]
69pub fn (mut ctx Context) hide_cursor() {
70 ctx.write('\x1b[?25l')
71}
72
73// set_color sets the current foreground color used by any succeeding `draw_*` calls.
74@[inline]
75pub fn (mut ctx Context) set_color(c Color) {
76 if ctx.enable_rgb {
77 ctx.write('\x1b[38;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
78 } else if ctx.enable_ansi256 {
79 ctx.write('\x1b[38;5;${rgb2ansi(c.r, c.g, c.b)}m')
80 } else {
81 ctx.write('\x1b[${30 + rgb2basic_ansi(c.r, c.g, c.b)}m')
82 }
83}
84
85// set_color sets the current background color used by any succeeding `draw_*` calls.
86@[inline]
87pub fn (mut ctx Context) set_bg_color(c Color) {
88 if ctx.enable_rgb {
89 ctx.write('\x1b[48;2;${int(c.r)};${int(c.g)};${int(c.b)}m')
90 } else if ctx.enable_ansi256 {
91 ctx.write('\x1b[48;5;${rgb2ansi(c.r, c.g, c.b)}m')
92 } else {
93 ctx.write('\x1b[${40 + rgb2basic_ansi(c.r, c.g, c.b)}m')
94 }
95}
96
97// reset_color sets the current foreground color back to it's default value.
98@[inline]
99pub fn (mut ctx Context) reset_color() {
100 ctx.write('\x1b[39m')
101}
102
103// reset_bg_color sets the current background color back to it's default value.
104@[inline]
105pub fn (mut ctx Context) reset_bg_color() {
106 ctx.write('\x1b[49m')
107}
108
109// reset restores the state of all colors and text formats back to their default values.
110@[inline]
111pub fn (mut ctx Context) reset() {
112 ctx.write('\x1b[0m')
113}
114
115// clear erases the entire terminal window and any saved lines.
116@[inline]
117pub fn (mut ctx Context) clear() {
118 ctx.write('\x1b[2J\x1b[3J')
119}
120
121// set_window_title sets the string `s` as the window title.
122@[inline]
123pub fn (mut ctx Context) set_window_title(s string) {
124 if !ctx.supports_window_title {
125 return
126 }
127 print('\x1b]0;${s}\x07')
128 flush_stdout()
129}
130
131fn rgb2basic_ansi(r int, g int, b int) int {
132 mut best_index := 0
133 mut best_distance := -1
134 for i in 0 .. 8 {
135 ref := color_table[i]
136 dr := r - int((ref >> 16) & 0xff)
137 dg := g - int((ref >> 8) & 0xff)
138 db := b - int(ref & 0xff)
139 distance := dr * dr + dg * dg + db * db
140 if best_distance == -1 || distance < best_distance {
141 best_distance = distance
142 best_index = i
143 }
144 }
145 return best_index
146}
147
148// draw_point draws a point at position `x`,`y`.
149@[inline]
150pub fn (mut ctx Context) draw_point(x int, y int) {
151 ctx.set_cursor_position(x, y)
152 ctx.write(' ')
153}
154
155// draw_text draws the string `s`, starting from position `x`,`y`.
156@[inline]
157pub fn (mut ctx Context) draw_text(x int, y int, s string) {
158 ctx.set_cursor_position(x, y)
159 ctx.write(s)
160}
161
162// draw_line draws a line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
163pub fn (mut ctx Context) draw_line(x int, y int, x2 int, y2 int) {
164 min_x, min_y := if x < x2 { x } else { x2 }, if y < y2 { y } else { y2 }
165 max_x, _ := if x > x2 { x } else { x2 }, if y > y2 { y } else { y2 }
166 if y == y2 {
167 // Horizontal line, performance improvement
168 ctx.set_cursor_position(min_x, min_y)
169 ctx.write(strings.repeat(` `, max_x + 1 - min_x))
170 return
171 }
172 // Draw the various points with Bresenham's line algorithm:
173 mut x0, x1 := x, x2
174 mut y0, y1 := y, y2
175 sx := if x0 < x1 { 1 } else { -1 }
176 sy := if y0 < y1 { 1 } else { -1 }
177 dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
178 dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
179 mut err := dx + dy
180 for {
181 // res << Segment{ x0, y0 }
182 ctx.draw_point(x0, y0)
183 if x0 == x1 && y0 == y1 {
184 break
185 }
186 e2 := 2 * err
187 if e2 >= dy {
188 err += dy
189 x0 += sx
190 }
191 if e2 <= dx {
192 err += dx
193 y0 += sy
194 }
195 }
196}
197
198// draw_dashed_line draws a dashed line segment, starting at point `x`,`y`, and ending at point `x2`,`y2`.
199pub fn (mut ctx Context) draw_dashed_line(x int, y int, x2 int, y2 int) {
200 // Draw the various points with Bresenham's line algorithm:
201 mut x0, x1 := x, x2
202 mut y0, y1 := y, y2
203 sx := if x0 < x1 { 1 } else { -1 }
204 sy := if y0 < y1 { 1 } else { -1 }
205 dx := if x0 < x1 { x1 - x0 } else { x0 - x1 }
206 dy := if y0 < y1 { y0 - y1 } else { y1 - y0 } // reversed
207 mut err := dx + dy
208 mut i := 0
209 for {
210 if i % 2 == 0 {
211 ctx.draw_point(x0, y0)
212 }
213 if x0 == x1 && y0 == y1 {
214 break
215 }
216 e2 := 2 * err
217 if e2 >= dy {
218 err += dy
219 x0 += sx
220 }
221 if e2 <= dx {
222 err += dx
223 y0 += sy
224 }
225 i++
226 }
227}
228
229// draw_rect draws a rectangle, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
230pub fn (mut ctx Context) draw_rect(x int, y int, x2 int, y2 int) {
231 if y == y2 || x == x2 {
232 ctx.draw_line(x, y, x2, y2)
233 return
234 }
235 min_y, max_y := if y < y2 { y, y2 } else { y2, y }
236 for y_pos in min_y .. max_y + 1 {
237 ctx.draw_line(x, y_pos, x2, y_pos)
238 }
239}
240
241// draw_empty_dashed_rect draws a rectangle with dashed lines, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
242pub fn (mut ctx Context) draw_empty_dashed_rect(x int, y int, x2 int, y2 int) {
243 if y == y2 || x == x2 {
244 ctx.draw_dashed_line(x, y, x2, y2)
245 return
246 }
247
248 min_x, max_x := if x < x2 { x, x2 } else { x2, x }
249 min_y, max_y := if y < y2 { y, y2 } else { y2, y }
250
251 ctx.draw_dashed_line(min_x, min_y, max_x, min_y)
252 ctx.draw_dashed_line(min_x, min_y, min_x, max_y)
253 if (max_y - min_y) & 1 == 0 {
254 ctx.draw_dashed_line(min_x, max_y, max_x, max_y)
255 } else {
256 ctx.draw_dashed_line(min_x + 1, max_y, max_x, max_y)
257 }
258 if (max_x - min_x) & 1 == 0 {
259 ctx.draw_dashed_line(max_x, min_y, max_x, max_y)
260 } else {
261 ctx.draw_dashed_line(max_x, min_y + 1, max_x, max_y)
262 }
263}
264
265// draw_empty_rect draws a rectangle with no fill, starting at top left `x`,`y`, and ending at bottom right `x2`,`y2`.
266pub fn (mut ctx Context) draw_empty_rect(x int, y int, x2 int, y2 int) {
267 if y == y2 || x == x2 {
268 ctx.draw_line(x, y, x2, y2)
269 return
270 }
271 ctx.draw_line(x, y, x2, y)
272 ctx.draw_line(x, y2, x2, y2)
273 ctx.draw_line(x, y, x, y2)
274 ctx.draw_line(x2, y, x2, y2)
275}
276
277// horizontal_separator draws a horizontal separator, spanning the width of the screen.
278@[inline]
279pub fn (mut ctx Context) horizontal_separator(y int) {
280 ctx.set_cursor_position(0, y)
281 ctx.write(strings.repeat(`-`, ctx.window_width)) // /* `⎽` */
282}
283