use egui::{Color32, Rect, Stroke, StrokeKind, Ui, Vec2, vec2};
use super::{FocusRing, NativeFeel};
use crate::effects::RevealHighlight;
const NATIVE_ID: &str = "facett_native_feel";
pub fn publish_native(ctx: &egui::Context, feel: NativeFeel) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new(NATIVE_ID), feel));
}
pub fn native_feel(ui: &Ui) -> NativeFeel {
let feel = ui.data(|d| d.get_temp::<NativeFeel>(egui::Id::new(NATIVE_ID))).unwrap_or_default();
probe::record(feel.platform);
feel
}
pub mod probe {
use std::cell::Cell;
use crate::look::Platform;
thread_local! {
static CONSUMED: Cell<Option<Platform>> = const { Cell::new(None) };
}
pub fn record(p: Platform) {
CONSUMED.with(|c| c.set(Some(p)));
}
pub fn reset() {
CONSUMED.with(|c| c.set(None));
}
pub fn consumed() -> Option<Platform> {
CONSUMED.with(|c| c.get())
}
}
pub fn reveal_on_hover(ui: &Ui, rect: Rect, corner_radius: f32) {
let feel = native_feel(ui);
if !feel.reveal_highlight {
return;
}
let pal = crate::theme(ui);
let reveal = RevealHighlight::new(pal.glow).with_intensity(0.8);
reveal.paint_in(ui, rect, corner_radius, true);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FocusRingVisual {
pub rect: Rect,
pub stroke: Stroke,
pub glow: bool,
}
pub fn focus_ring_visual(ring: &FocusRing, rect: Rect, accent: Color32, outline: Color32) -> FocusRingVisual {
let color = if ring.accent_tinted { accent } else { outline };
FocusRingVisual {
rect: rect.expand(ring.expansion),
stroke: Stroke::new(ring.width, color),
glow: ring.glow,
}
}
pub fn apply_focus_ring(ui: &Ui, rect: Rect, focused: bool, corner_radius: f32) {
if !focused {
return;
}
let feel = native_feel(ui);
let pal = crate::theme(ui);
let vis = focus_ring_visual(&feel.focus_ring, rect, pal.accent, pal.text);
let painter = ui.painter();
if vis.glow && crate::look::effects_policy(ui).allows_decorative_motion() {
crate::effects::glow_rect(painter, vis.rect, vis.stroke.color, 0.8, feel.focus_ring.width.max(2.0) as u32);
}
painter.rect_stroke(vis.rect, corner_radius + feel.focus_ring.expansion, vis.stroke, StrokeKind::Outside);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ElevationShadow {
pub offset: Vec2,
pub spread: f32,
pub alpha: u8,
}
pub fn elevation_shadow_params(elevation: f32) -> ElevationShadow {
let e = elevation.clamp(0.0, 1.0);
ElevationShadow {
offset: vec2(0.0, 1.0 + e * 6.0),
spread: 2.0 + e * 10.0,
alpha: (e * 130.0) as u8,
}
}
pub fn elevation_shadow(ui: &Ui, rect: Rect, corner_radius: f32) {
let feel = native_feel(ui);
if !crate::look::effects_policy(ui).allows_transparency() {
return;
}
let sh = elevation_shadow_params(feel.elevation);
if sh.alpha == 0 {
return;
}
let painter = ui.painter();
let base = rect.translate(sh.offset);
let layers = 5u32;
for i in 0..layers {
let f = i as f32 / layers as f32; let grow = f * sh.spread;
let a = ((1.0 - f) * sh.alpha as f32) as u8;
if a == 0 {
continue;
}
painter.rect_filled(base.expand(grow), corner_radius + grow, Color32::from_black_alpha(a));
}
}
#[cfg(test)]
mod tests {
use egui::{pos2, vec2};
use super::*;
use crate::look::Platform;
#[test]
fn focus_ring_visual_differs_mac_vs_windows() {
let rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 40.0));
let accent = Color32::from_rgb(80, 160, 255);
let outline = Color32::GRAY;
let mac = focus_ring_visual(&Platform::Mac.native_feel().focus_ring, rect, accent, outline);
let win = focus_ring_visual(&Platform::Windows.native_feel().focus_ring, rect, accent, outline);
assert!(mac.glow && !win.glow, "mac ring glows; windows is a crisp rect");
assert!(mac.stroke.width > win.stroke.width, "mac ring is wider ({} vs {})", mac.stroke.width, win.stroke.width);
assert!(mac.rect.width() > win.rect.width(), "mac halo expands further than the windows hug");
assert_eq!(mac.stroke.color, accent);
assert_eq!(win.stroke.color, accent);
}
#[test]
fn elevation_shadow_is_heavier_on_windows_than_mac() {
let mac = elevation_shadow_params(Platform::Mac.native_feel().elevation); let win = elevation_shadow_params(Platform::Windows.native_feel().elevation); assert!(win.alpha > mac.alpha, "windows elevation is darker: {} vs {}", win.alpha, mac.alpha);
assert!(win.offset.y > mac.offset.y, "windows shadow drops further");
assert!(win.spread > mac.spread, "windows shadow spreads wider");
assert_eq!(elevation_shadow_params(0.0).alpha, 0);
}
#[test]
fn native_feel_publish_round_trips_through_ctx() {
let ctx = egui::Context::default();
publish_native(&ctx, NativeFeel::windows());
#[allow(deprecated)]
let _ = ctx.run(egui::RawInput::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let feel = native_feel(ui);
assert_eq!(feel.platform, Platform::Windows);
assert!(feel.reveal_highlight, "windows preset routed through the ctx");
});
});
}
}