use crate::layout::subpixel_snap_rect;
use crate::theme::MacTheme;
use egui::{
color_picker::{color_picker_color32, Alpha},
Area, Color32, CornerRadius, Key, Modifiers, Order, Pos2, Rect, Response, Sense, Stroke,
StrokeKind, Ui, Vec2, Widget,
};
pub struct MacColorWell<'a> {
id: Option<egui::Id>,
color: &'a mut Color32,
theme: Option<&'a MacTheme>,
width: Option<f32>,
height: Option<f32>,
corner_radius: Option<f32>,
border_width: Option<f32>,
show_alpha: bool,
enabled: bool,
}
impl<'a> MacColorWell<'a> {
pub fn new(color: &'a mut Color32) -> Self {
Self {
id: None,
color,
theme: None,
width: None,
height: None,
corner_radius: None,
border_width: None,
show_alpha: true,
enabled: true,
}
}
pub fn id_salt(mut self, salt: impl std::hash::Hash + std::fmt::Debug) -> Self {
#[allow(deprecated)]
let id = egui::Id::new(salt);
self.id = Some(id);
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
pub fn corner_radius(mut self, r: f32) -> Self {
self.corner_radius = Some(r);
self
}
pub fn border_width(mut self, w: f32) -> Self {
self.border_width = Some(w);
self
}
pub fn show_alpha(mut self, show: bool) -> Self {
self.show_alpha = show;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn show(self, ui: &mut Ui) -> Response {
ui.add(self)
}
}
impl<'a> Widget for MacColorWell<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let theme = MacTheme::resolve(self.theme, ui.ctx());
let metrics = theme.color_well();
let width = self.width.unwrap_or_else(|| metrics.width.points());
let height = self.height.unwrap_or_else(|| metrics.height.points());
let corner_radius = self
.corner_radius
.unwrap_or_else(|| metrics.corner_radius.points());
let border_width = self
.border_width
.unwrap_or_else(|| metrics.border_width.points());
let swatch_inset = metrics.swatch_inset.points();
let swatch_radius = metrics.swatch_radius.points();
let sense = if self.enabled {
Sense::click()
} else {
Sense::hover()
};
let (raw_rect, mut response) = if let Some(explicit_id) = self.id {
let (r, _) = ui.allocate_exact_size(Vec2::new(width, height), Sense::hover());
let resp = ui.interact(r, explicit_id, sense);
(r, resp)
} else {
ui.allocate_exact_size(Vec2::new(width, height), sense)
};
let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
let alpha_factor = if self.enabled {
1.0
} else {
theme.disabled_opacity()
};
let painter = ui.painter_at(rect);
if self.enabled && response.has_focus() {
theme.paint_focus_ring(&painter, rect, corner_radius);
}
let bg_color = if !self.enabled {
theme.control_background().gamma_multiply(alpha_factor)
} else if response.is_pointer_button_down_on() {
theme.selected_control_inactive()
} else if response.hovered() {
theme.control()
} else {
theme.control_background()
};
let border_stroke = if self.enabled && response.has_focus() {
Stroke::new(border_width, theme.control_accent())
} else {
Stroke::new(border_width, theme.separator().gamma_multiply(alpha_factor))
};
painter.rect(
rect,
CornerRadius::same(corner_radius as u8),
bg_color,
border_stroke,
StrokeKind::Inside,
);
let swatch_rect = rect.shrink(swatch_inset);
if swatch_rect.width() > 0.0 && swatch_rect.height() > 0.0 {
if self.show_alpha && self.color.a() < u8::MAX {
let cell_size = metrics.checker_grid_size.points();
let cols = (swatch_rect.width() / cell_size).ceil() as usize;
let rows = (swatch_rect.height() / cell_size).ceil() as usize;
for r in 0..rows {
for c in 0..cols {
let is_dark = (r + c) % 2 == 1;
let tile_color = if is_dark {
theme.control()
} else {
theme.control_background()
};
let tile_min = Pos2::new(
(swatch_rect.left() + (c as f32) * cell_size).min(swatch_rect.right()),
(swatch_rect.top() + (r as f32) * cell_size).min(swatch_rect.bottom()),
);
let tile_max = Pos2::new(
(tile_min.x + cell_size).min(swatch_rect.right()),
(tile_min.y + cell_size).min(swatch_rect.bottom()),
);
painter.rect_filled(
Rect::from_min_max(tile_min, tile_max),
CornerRadius::ZERO,
tile_color.gamma_multiply(alpha_factor),
);
}
}
}
painter.rect(
swatch_rect,
CornerRadius::same(swatch_radius as u8),
self.color.gamma_multiply(alpha_factor),
Stroke::new(border_width, theme.separator().gamma_multiply(alpha_factor)),
StrokeKind::Inside,
);
}
let is_open_id = response.id.with("mac_color_picker_is_open");
let mut is_popup_open = ui.data(|d| d.get_temp::<bool>(is_open_id).unwrap_or(false));
if self.enabled {
if response.clicked() {
response.request_focus();
is_popup_open = !is_popup_open;
ui.data_mut(|d| d.insert_temp(is_open_id, is_popup_open));
} else if response.has_focus()
&& (ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Space))
|| ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter)))
{
is_popup_open = !is_popup_open;
ui.data_mut(|d| d.insert_temp(is_open_id, is_popup_open));
}
}
if self.enabled && is_popup_open {
let picker_dim = metrics.picker_size.points();
let picker_size = Vec2::new(picker_dim, picker_dim);
let popup_gap = metrics.popup_gap.points();
let screen_rect = ui
.ctx()
.input(|i| i.raw.screen_rect)
.unwrap_or_else(|| ui.clip_rect());
let popup_y = if rect.bottom() + popup_gap + picker_size.y > screen_rect.bottom()
&& rect.top() - popup_gap - picker_size.y >= screen_rect.top()
{
rect.top() - popup_gap - picker_size.y
} else {
rect.bottom() + popup_gap
};
let popup_x = if rect.left() + picker_size.x > screen_rect.right() {
(screen_rect.right() - picker_size.x).max(screen_rect.left())
} else {
rect.left()
};
let popup_pos = Pos2::new(popup_x, popup_y);
let area_id = response.id.with("mac_color_picker_area");
let area_resp = Area::new(area_id)
.order(Order::Foreground)
.fixed_pos(popup_pos)
.show(ui.ctx(), |picker_ui| {
let frame = egui::Frame::window(picker_ui.style())
.fill(theme.window_background())
.stroke(Stroke::new(
metrics.border_width.points(),
theme.separator(),
))
.corner_radius(CornerRadius::same(
metrics.popup_corner_radius.points() as u8
));
frame.show(picker_ui, |inner_ui| {
let alpha_mode = if self.show_alpha {
Alpha::OnlyBlend
} else {
Alpha::Opaque
};
if color_picker_color32(inner_ui, self.color, alpha_mode) {
response.mark_changed();
}
});
});
let pointer_pos = ui.input(|i| i.pointer.interact_pos().or(i.pointer.latest_pos()));
if ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Escape)) {
ui.data_mut(|d| d.insert_temp(is_open_id, false));
} else if ui.input(|i| i.pointer.button_clicked(egui::PointerButton::Primary))
&& !response.clicked()
{
let popup_rect = area_resp
.response
.rect
.union(Rect::from_min_size(popup_pos, picker_size));
let inside_popup = pointer_pos.map(|p| popup_rect.contains(p)).unwrap_or(false);
if !inside_popup {
ui.data_mut(|d| d.insert_temp(is_open_id, false));
}
}
}
response
}
}