Build a Color Palette App with egui Color Picker | Rust GUI Ep 12
Video: Build a Color Palette App with egui Color Picker | Rust GUI Ep 12 by Taught by Celeste AI - AI Coding Coach
ui.color_edit_button_rgb(&mut self.color)— one widget, full RGB editor with a sample swatch and HSV controls.
Today's widget: the color picker. A click opens a popover with sliders for hue/saturation/value, an alpha bar, and a sample swatch. Once the user closes the popover, your [f32; 3] (or Color32) holds the picked color. We build a small palette app: pick a color, save it to a list, see saved colors as colored swatches with hex codes.
What we are building
A two-pane palette app:
- Left side panel: pick a color and view its RGB values; click "Save Color" to add it to the palette.
- Central panel: the saved colors, each with a swatch, the hex code, and a "Remove" button.
The state
pub struct MyApp {
current_color: [f32; 3],
saved_colors: Vec<[f32; 3]>,
}
Colors as [f32; 3] — three floats in 0.0..=1.0 representing RGB. egui also uses egui::Color32 (8-bit per channel) and egui::Rgba (linear-space floats) — the type you store depends on what you need to do. The picker widget binds to [f32; 3], so that is what we use for the input. We convert to Color32 only when we need to display a swatch.
The color picker widget
ui.color_edit_button_rgb(&mut self.current_color);
That is the entire picker. The widget renders a small button showing the current color. Click it and a popover opens with full RGB / HSV / alpha controls. Pick a color in the popover, close it, and self.current_color reflects the choice.
Variants:
color_edit_button_rgb(&mut [f32; 3])— RGB without alpha (today's example).color_edit_button_rgba(&mut [f32; 4])— RGB plus alpha.color_edit_button_srgba(&mut Color32)— sRGB-encoded with alpha.
Use rgb for opaque colors, rgba when transparency matters.
Converting to Color32 for display
let display = egui::Color32::from(egui::Rgba::from_rgb(
self.current_color[0], self.current_color[1], self.current_color[2],
));
ui.label(format!(
"R: {} G: {} B: {}",
display.r(), display.g(), display.b(),
));
Rgba::from_rgb(r, g, b) builds a linear RGB color from three floats. Color32::from(rgba) converts it into the 8-bit-per-channel form egui uses for rendering. Then .r(), .g(), .b() give the integer values 0..=255.
Why two types? f32 makes color math (gradients, blending) numerically clean; u8 is what the GPU consumes. egui exposes both and converts between them; you pick whichever matches what you are doing.
Painting a swatch manually
let (rect, _response) = ui.allocate_exact_size(
egui::vec2(40.0, 20.0),
egui::Sense::hover(),
);
ui.painter().rect_filled(rect, 4.0, c);
For each saved color, we render a 40×20 colored rectangle. The mechanism:
ui.allocate_exact_size(size, sense)reserves a rectangle of the given size in the layout. It returns(Rect, Response)— the actual screen rectangle and a Response for click/hover events.ui.painter().rect_filled(rect, rounding, color)paints inside that rectangle. The second argument is the corner radius;cis the fill color.
This is custom painting at its most basic. egui's Painter exposes the lower-level draw API: lines, circles, paths, images, text. Most apps never need it, but for things like color swatches, custom indicators, sparklines — it is exactly the right tool.
Iterating and removing
let mut to_remove: Option<usize> = None;
for (i, color) in self.saved_colors.iter().enumerate() {
ui.horizontal(|ui| {
// ... swatch + hex code + remove button
if ui.button("Remove").clicked() {
to_remove = Some(i);
}
});
}
if let Some(i) = to_remove {
self.saved_colors.remove(i);
}
Two-step deletion. We cannot call self.saved_colors.remove(i) inside the loop — we are iterating over self.saved_colors, and the borrow checker rightly rejects mutating a vector while iterating it.
The pattern: track the index to remove in a mutable Option<usize>. After the loop, apply the deletion.
If multiple removals could happen in one frame (very rare for a button-driven UI, more common for batch operations), use Vec<usize> and remove in reverse order so earlier indices stay valid.
Hex display
ui.label(format!(
"#{:02X}{:02X}{:02X}",
c.r(), c.g(), c.b(),
));
Two characters per channel, uppercase hex, padded with zeroes. #FF0000 for red, #00FF00 for green. Standard CSS color hex. The format! macro's {:02X} does the formatting; nothing egui-specific here.
Running it
cargo run. The left panel shows a color swatch button. Click it to pick a new color. The RGB values appear below. Click "Save Color" — the central panel adds a new row with the swatch, hex code, and a Remove button. Click Remove — the row disappears.
Try saving the same color twice — both rows appear, since we did not deduplicate. Adding deduplication is one line: if !self.saved_colors.contains(&self.current_color) { self.saved_colors.push(...) }.
Common mistakes
Mutating saved_colors inside the iteration. Borrow checker error. Use the deferred-removal pattern.
Using [f32; 3] for sRGB display. RGB floats are linear-space; sRGB is gamma-corrected. The conversion via Rgba::from_rgb does the right thing; if you write your own without conversion, colors will look washed out.
Forgetting Sense::hover(). The painted swatch with Sense::hover() does not respond to clicks; it just visually exists. If you want clickable swatches, use Sense::click() and check the returned Response.
Storing colors as strings ("FF0000"). Roundtripping to and from text every frame is wasteful. Store the typed value and format only at display time.
What's next
Next episode: dropdown menus. egui::ComboBox for choosing one option from many. Combined with RichText and FontId, you get a font preview app.
Recap
ui.color_edit_button_rgb(&mut [f32; 3]) for opaque RGB picking; _rgba or _srgba variants for alpha. Convert with egui::Rgba::from_rgb and egui::Color32::from. Custom-paint swatches with ui.allocate_exact_size plus ui.painter().rect_filled. Defer deletion to after the iteration.
Next episode: dropdown menus. See you in the next one.