libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
GuiPaint.cpp
1/****************************************************************************
2 * libgui
3 * Daryl Borth 2026
4 * GuiPaint.cpp
5 ***************************************************************************/
6
7#include "GuiPaint.h"
8#include <cmath>
9#include <algorithm>
10
11namespace {
12
13float clamp01(float t)
14{
15 return t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t);
16}
17
18float lerp(float a, float b, float t)
19{
20 return a + (b - a) * t;
21}
22
23uint8_t lerp8(uint8_t a, uint8_t b, float t)
24{
25 return (uint8_t)(lerp((float)a, (float)b, t) + 0.5f);
26}
27
28bool sameColor(const PixelColor& a, const PixelColor& b)
29{
30 return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
31}
32
33bool sameFill(const GuiFill& a, const GuiFill& b)
34{
35 if(a.gradient != b.gradient || !sameColor(a.from, b.from))
36 return false;
37 return !a.gradient || (sameColor(a.to, b.to) && a.angle == b.angle);
38}
39
40PixelColor mixColor(const PixelColor& a, const PixelColor& b, float t)
41{
42 PixelColor c;
43 c.r = lerp8(a.r, b.r, t);
44 c.g = lerp8(a.g, b.g, t);
45 c.b = lerp8(a.b, b.b, t);
46 c.a = lerp8(a.a, b.a, t);
47 return c;
48}
49
50//A fill in gradient form, so a solid and a gradient can be blended
51GuiFill asGradient(const GuiFill& f, float otherAngle)
52{
53 if(f.gradient)
54 return f;
55 return GuiFill::linear(f.from, f.from, otherAngle);
56}
57
58GuiFill mixFill(const GuiFill& a, const GuiFill& b, float t)
59{
60 if(!a.gradient && !b.gradient)
61 return GuiFill::solid(mixColor(a.from, b.from, t));
62 GuiFill ga = asGradient(a, b.angle), gb = asGradient(b, a.angle);
63 return GuiFill::linear(mixColor(ga.from, gb.from, t), mixColor(ga.to, gb.to, t), lerp(ga.angle, gb.angle, t));
64}
65
66PixelColor transparent(PixelColor c)
67{
68 c.a = 0;
69 return c;
70}
71
72} // namespace
73
74GuiPaint::GuiPaint() :
75 fill(GuiFill::solid(PixelColor{ 0, 0, 0, 0 })), hasFill(false),
76 strokeFill(GuiFill::solid(PixelColor{ 0, 0, 0, 0 })), strokeWidth(0.0f), strokeAlign(STROKE::INSIDE),
77 shadowColor{ 0, 0, 0, 0 }, shadowBlur(0.0f), shadowSpread(0.0f), shadowDx(0.0f), shadowDy(0.0f)
78{}
79
80GuiPaint& GuiPaint::color(PixelColor c)
81{
82 fill = GuiFill::solid(c);
83 hasFill = true;
84 return *this;
85}
86
87GuiPaint& GuiPaint::gradient(PixelColor from, PixelColor to, float angleDeg)
88{
89 fill = GuiFill::linear(from, to, angleDeg);
90 hasFill = true;
91 return *this;
92}
93
94GuiPaint& GuiPaint::stroke(float width, PixelColor c, STROKE align)
95{
96 strokeFill = GuiFill::solid(c);
97 strokeWidth = width;
98 strokeAlign = align;
99 return *this;
100}
101
102GuiPaint& GuiPaint::shadow(float blur, PixelColor c, float dx, float dy, float spread)
103{
104 shadowColor = c;
105 shadowBlur = blur;
106 shadowDx = dx;
107 shadowDy = dy;
108 shadowSpread = spread;
109 return *this;
110}
111
112GuiPaint& GuiPaint::glow(float blur, PixelColor c, float spread)
113{
114 return shadow(blur, c, 0.0f, 0.0f, spread);
115}
116
117bool GuiPaint::operator==(const GuiPaint& o) const
118{
119 if(hasFill != o.hasFill || (hasFill && !sameFill(fill, o.fill)))
120 return false;
121 if(strokeWidth != o.strokeWidth)
122 return false;
123 if(strokeWidth > 0.0f && (strokeAlign != o.strokeAlign || !sameFill(strokeFill, o.strokeFill)))
124 return false;
125 if(shadowColor.a != o.shadowColor.a)
126 return false;
127 if(shadowColor.a != 0 && (!sameColor(shadowColor, o.shadowColor) || shadowBlur != o.shadowBlur ||
128 shadowSpread != o.shadowSpread || shadowDx != o.shadowDx || shadowDy != o.shadowDy))
129 return false;
130 return true;
131}
132
133GuiPaint GuiPaint::mix(const GuiPaint& a, const GuiPaint& b, float t)
134{
135 t = clamp01(t);
136 if(t <= 0.0f)
137 return a;
138 if(t >= 1.0f)
139 return b;
140
141 GuiPaint p;
142
143 // fill: a fill present on one side only fades in or out in its own color
144 if(a.hasFill || b.hasFill)
145 {
146 GuiFill fa = a.hasFill ? a.fill : GuiFill::solid(transparent(b.fill.from));
147 GuiFill fb = b.hasFill ? b.fill : GuiFill::solid(transparent(a.fill.from));
148 if(!a.hasFill && b.fill.gradient)
149 fa = GuiFill::linear(transparent(b.fill.from), transparent(b.fill.to), b.fill.angle);
150 if(!b.hasFill && a.fill.gradient)
151 fb = GuiFill::linear(transparent(a.fill.from), transparent(a.fill.to), a.fill.angle);
152 p.fill = mixFill(fa, fb, t);
153 p.hasFill = true;
154 }
155
156 // outline: a missing outline grows from zero width in the other's color
157 if(a.strokeWidth > 0.0f || b.strokeWidth > 0.0f)
158 {
159 GuiFill sa = a.strokeWidth > 0.0f ? a.strokeFill : b.strokeFill;
160 GuiFill sb = b.strokeWidth > 0.0f ? b.strokeFill : a.strokeFill;
161 p.strokeFill = mixFill(sa, sb, t);
162 p.strokeWidth = lerp(a.strokeWidth, b.strokeWidth, t);
163 p.strokeAlign = t < 0.5f ? (a.strokeWidth > 0.0f ? a.strokeAlign : b.strokeAlign) : (b.strokeWidth > 0.0f ? b.strokeAlign : a.strokeAlign);
164 }
165
166 // shadow: a missing shadow fades in or out in the other's color and shape
167 if(a.shadowColor.a != 0 || b.shadowColor.a != 0)
168 {
169 const GuiPaint& sa = a.shadowColor.a != 0 ? a : b;
170 const GuiPaint& sb = b.shadowColor.a != 0 ? b : a;
171 PixelColor ca = a.shadowColor.a != 0 ? a.shadowColor : transparent(b.shadowColor);
172 PixelColor cb = b.shadowColor.a != 0 ? b.shadowColor : transparent(a.shadowColor);
173 p.shadowColor = mixColor(ca, cb, t);
174 p.shadowBlur = lerp(sa.shadowBlur, sb.shadowBlur, t);
175 p.shadowSpread = lerp(sa.shadowSpread, sb.shadowSpread, t);
176 p.shadowDx = lerp(sa.shadowDx, sb.shadowDx, t);
177 p.shadowDy = lerp(sa.shadowDy, sb.shadowDy, t);
178 }
179 return p;
180}
181
182GuiShapeStyle::GuiShapeStyle(const GuiPaint& normalPaint) :
183 normal(normalPaint), hasFocused(false), hasPressed(false), hasDisabled(false)
184{}
185
186GuiShapeStyle& GuiShapeStyle::onFocus(const GuiPaint& p)
187{
188 focused = p;
189 hasFocused = true;
190 return *this;
191}
192
193GuiShapeStyle& GuiShapeStyle::onPress(const GuiPaint& p)
194{
195 pressed = p;
196 hasPressed = true;
197 return *this;
198}
199
200GuiShapeStyle& GuiShapeStyle::onDisable(const GuiPaint& p)
201{
202 disabled = p;
203 hasDisabled = true;
204 return *this;
205}
206
207const GuiPaint& GuiShapeStyle::forState(SHAPE_STATE s) const
208{
209 switch(s)
210 {
211 case SHAPE_STATE::FOCUSED: return hasFocused ? focused : normal;
212 case SHAPE_STATE::PRESSED: return hasPressed ? pressed : (hasFocused ? focused : normal);
213 case SHAPE_STATE::DISABLED: return hasDisabled ? disabled : normal;
214 default: return normal;
215 }
216}
217
218void GuiAddPaintedShape(GuiMesh& mesh, SHAPE shape, const GuiRect& r, const GuiCorners& corners, const GuiPaint& paint)
219{
220 const float half = 0.5f * std::min(r.w, r.h);
221 const GuiCorners outline = shape == SHAPE::ELLIPSE ? GuiCorners(half) : corners;
222
223 if(paint.shadowColor.a != 0)
224 mesh.addShadow(r, outline, paint.shadowBlur, paint.shadowSpread, paint.shadowDx, paint.shadowDy, paint.shadowColor);
225
226 if(paint.hasFill)
227 {
228 // An opaque inside outline at least one feather wide sits exactly over the fill's
229 // anti-aliased edge, and gives the shape its own; the fill's would only bleed its color
230 // into that edge (and cost a third of the fill's triangles)
231 const bool strokeOpaque = paint.strokeFill.from.a == 255 && (!paint.strokeFill.gradient || paint.strokeFill.to.a == 255);
232 const bool edgeFringe = !(paint.strokeWidth > 0.0f && paint.strokeAlign == STROKE::INSIDE && strokeOpaque &&
233 std::min(paint.strokeWidth, half) >= mesh.getFeather());
234
235 if(shape == SHAPE::ELLIPSE)
236 mesh.addEllipse(r, paint.fill, edgeFringe);
237 else
238 mesh.addRoundRect(r, corners, paint.fill, edgeFringe);
239 }
240
241 if(paint.strokeWidth > 0.0f)
242 {
243 if(shape == SHAPE::ELLIPSE)
244 mesh.strokeEllipse(r, paint.strokeWidth, paint.strokeAlign, paint.strokeFill);
245 else
246 mesh.strokeRoundRect(r, corners, paint.strokeWidth, paint.strokeAlign, paint.strokeFill);
247 }
248}
249
251{
252 return mixColor(a, b, clamp01(t));
253}
Accumulates shapes as a triangle list of MeshVertex.
void addShadow(const GuiRect &r, const GuiCorners &corners, float blur, float spread, float dx, float dy, PixelColor color)
Adds a soft drop shadow or glow for a rounded rectangle.
void strokeEllipse(const GuiRect &r, float width, STROKE align, const GuiFill &fill)
Adds an ellipse outline.
void addRoundRect(const GuiRect &r, const GuiCorners &corners, const GuiFill &fill, bool edgeFringe=true)
Adds a filled rounded rectangle.
void strokeRoundRect(const GuiRect &r, const GuiCorners &corners, float width, STROKE align, const GuiFill &fill)
Adds a rounded-rectangle outline.
float getFeather() const
Gets the feather width the mesh was built with.
void addEllipse(const GuiRect &r, const GuiFill &fill, bool edgeFringe=true)
Adds a filled ellipse inscribed in a rectangle (a circle when w == h)
bool hasFill
The look of a shape: an optional fill, an optional outline and an optional shadow or glow.
Definition GuiPaint.h:28
STROKE
Where a stroke sits relative to the shape's geometric edge.
Definition GuiGeometry.h:52
SHAPE_STATE
The states a styled shape distinguishes.
Definition GuiPaint.h:70
void GuiAddPaintedShape(GuiMesh &mesh, SHAPE shape, const GuiRect &r, const GuiCorners &corners, const GuiPaint &paint)
Adds a painted shape to a mesh: shadow first, then fill, then outline.
Definition GuiPaint.cpp:218
SHAPE
Basic shape kinds a paint can be applied to.
Definition GuiPaint.h:17
PixelColor GuiMixColor(PixelColor a, PixelColor b, float t)
Blends two colors per channel (alpha included): t = 0 gives a, t = 1 gives b.
Definition GuiPaint.cpp:250
@ INSIDE
Entirely inside the edge (the shape keeps its size)
Definition GuiGeometry.h:53
@ FOCUSED
Highlighted: under the cursor, or selected with the D-pad.
Definition GuiPaint.h:72
@ ELLIPSE
Ellipse inscribed in the rectangle (a circle when it is square)
Definition GuiPaint.h:19
Per-corner radii, in the order top-left, top-right, bottom-right, bottom-left.
Definition GuiGeometry.h:40
A solid color or a two-color linear gradient.
Definition GuiGeometry.h:60
bool gradient
true for a gradient, false for a solid color.
Definition GuiGeometry.h:64
PixelColor from
Solid color, or the gradient's start color.
Definition GuiGeometry.h:61
static GuiFill linear(PixelColor a, PixelColor b, float angleDeg)
A linear gradient across the shape's bounding box.
static GuiFill solid(PixelColor c)
A single flat color.
PixelColor to
The gradient's end color (unused when solid).
Definition GuiGeometry.h:62
float angle
Gradient direction in degrees: 0 = left to right, 90 = top to bottom.
Definition GuiGeometry.h:63
An axis-aligned rectangle (top-left origin) in design-canvas pixels.
Definition GuiGeometry.h:30
float w
Width.
Definition GuiGeometry.h:33
float h
Height.
Definition GuiGeometry.h:34
A set of paints, one per state. States left unset fall back to the normal paint. GuiShapeStyle(rest)....
Definition GuiPaint.h:80
const GuiPaint & forState(SHAPE_STATE s) const
Gets the paint for a state, falling back to normal when none was set.
Definition GuiPaint.cpp:207
8-bit-per-channel RGBA color used by the video interfaces.
Definition Mesh.h:16
uint8_t g
Green color component.
Definition Mesh.h:18
uint8_t r
Red color component.
Definition Mesh.h:17
uint8_t b
Blue color component.
Definition Mesh.h:19
uint8_t a
Alpha component.
Definition Mesh.h:20