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