a modern tui library written in zig
1const std = @import("std");
2const fmt = std.fmt;
3const math = std.math;
4const base64 = std.base64.standard.Encoder;
5const zigimg = @import("zigimg");
6
7const Window = @import("Window.zig");
8
9const Image = @This();
10
11const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};";
12
13pub const Source = union(enum) {
14 path: []const u8,
15 mem: []const u8,
16};
17
18pub const TransmitFormat = enum {
19 rgb,
20 rgba,
21 png,
22};
23
24pub const TransmitMedium = enum {
25 file,
26 temp_file,
27 shared_mem,
28};
29
30pub const Placement = struct {
31 img_id: u32,
32 options: Image.DrawOptions,
33};
34
35pub const CellSize = struct {
36 rows: u16,
37 cols: u16,
38};
39
40pub const DrawOptions = struct {
41 /// an offset into the top left cell, in pixels, with where to place the
42 /// origin of the image. These must be less than the pixel size of a single
43 /// cell
44 pixel_offset: ?struct {
45 x: u16,
46 y: u16,
47 } = null,
48 /// the vertical stacking order
49 /// < 0: Drawn beneath text
50 /// < -1_073_741_824: Drawn beneath "default" background cells
51 z_index: ?i32 = null,
52 /// A clip region of the source image to draw.
53 clip_region: ?struct {
54 x: ?u16 = null,
55 y: ?u16 = null,
56 width: ?u16 = null,
57 height: ?u16 = null,
58 } = null,
59 /// Scaling to apply to the Image
60 scale: enum {
61 /// no scaling applied. the image may extend beyond the window
62 none,
63 /// Stretch / shrink the image to fill the window
64 fill,
65 /// Scale the image to fit the window, maintaining aspect ratio
66 fit,
67 /// Scale the image to fit the window, only if needed.
68 contain,
69 } = .none,
70 /// the size to render the image. Generally you will not need to use this
71 /// field, and should prefer to use scale. `draw` will fill in this field with
72 /// the correct values if a scale method is applied.
73 size: ?struct {
74 rows: ?u16 = null,
75 cols: ?u16 = null,
76 } = null,
77};
78
79/// unique identifier for this image. This will be managed by the screen.
80id: u32,
81
82/// width in pixels
83width: u16,
84/// height in pixels
85height: u16,
86
87pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
88 var p_opts = opts;
89 switch (opts.scale) {
90 .none => {},
91 .fill => {
92 p_opts.size = .{
93 .rows = win.height,
94 .cols = win.width,
95 };
96 },
97 .fit,
98 .contain,
99 => contain: {
100 // cell geometry
101 const x_pix = win.screen.width_pix;
102 const y_pix = win.screen.height_pix;
103 const w = win.screen.width;
104 const h = win.screen.height;
105
106 const pix_per_col = try std.math.divCeil(usize, x_pix, w);
107 const pix_per_row = try std.math.divCeil(usize, y_pix, h);
108
109 const win_width_pix = pix_per_col * win.width;
110 const win_height_pix = pix_per_row * win.height;
111
112 const fit_x: bool = if (win_width_pix >= self.width) true else false;
113 const fit_y: bool = if (win_height_pix >= self.height) true else false;
114
115 // Does the image fit with no scaling?
116 if (opts.scale == .contain and fit_x and fit_y) break :contain;
117
118 // Does the image require vertical scaling?
119 if (fit_x and !fit_y)
120 p_opts.size = .{
121 .rows = win.height,
122 }
123
124 // Does the image require horizontal scaling?
125 else if (!fit_x and fit_y)
126 p_opts.size = .{
127 .cols = win.width,
128 }
129 else if (!fit_x and !fit_y) {
130 const diff_x = self.width - win_width_pix;
131 const diff_y = self.height - win_height_pix;
132 // The width difference is larger than the height difference.
133 // Scale by width
134 if (diff_x > diff_y)
135 p_opts.size = .{
136 .cols = win.width,
137 }
138 else
139 // The height difference is larger than the width difference.
140 // Scale by height
141 p_opts.size = .{
142 .rows = win.height,
143 };
144 } else {
145 std.debug.assert(opts.scale == .fit);
146 std.debug.assert(win_width_pix >= self.width);
147 std.debug.assert(win_height_pix >= self.height);
148
149 // Fits in both directions. Find the closer direction
150 const diff_x = win_width_pix - self.width;
151 const diff_y = win_height_pix - self.height;
152 // The width is closer in dimension. Scale by that
153 if (diff_x < diff_y)
154 p_opts.size = .{
155 .cols = win.width,
156 }
157 else
158 p_opts.size = .{
159 .rows = win.height,
160 };
161 }
162 },
163 }
164 const p = Placement{
165 .img_id = self.id,
166 .options = p_opts,
167 };
168 win.writeCell(0, 0, .{ .image = p });
169}
170
171/// the size of the image, in cells
172pub fn cellSize(self: Image, win: Window) !CellSize {
173 // cell geometry
174 const x_pix = win.screen.width_pix;
175 const y_pix = win.screen.height_pix;
176 const w = win.screen.width;
177 const h = win.screen.height;
178
179 const pix_per_col = try std.math.divCeil(u16, x_pix, w);
180 const pix_per_row = try std.math.divCeil(u16, y_pix, h);
181
182 const cell_width = std.math.divCeil(u16, self.width, pix_per_col) catch 0;
183 const cell_height = std.math.divCeil(u16, self.height, pix_per_row) catch 0;
184 return .{
185 .rows = cell_height,
186 .cols = cell_width,
187 };
188}