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