#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::prelude::{percent, Size, Style};
use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, Rect as KurboRect, RoundedRect};
use llimphi_ui::llimphi_raster::peniko::{color::AlphaColor, Color, Fill, Gradient};
use llimphi_ui::{PaintRect, Shadow, View};
use llimphi_theme::{alpha, elevation, radius, Theme};
#[derive(Debug, Clone, Copy)]
pub struct PanelStyle {
pub bg_base: Color,
pub accent: Color,
pub radius: f64,
pub hairline_alpha: f32,
pub gradient_strength: f32,
}
impl PanelStyle {
pub fn from_theme(t: &Theme) -> Self {
Self {
bg_base: t.bg_panel,
accent: t.accent,
radius: radius::MD,
hairline_alpha: alpha::SCRIM as f32 / 255.0 * 1.2, gradient_strength: 0.04,
}
}
pub fn from_theme_large(t: &Theme) -> Self {
Self {
bg_base: t.bg_panel,
accent: t.accent,
radius: radius::LG,
hairline_alpha: 0.35,
gradient_strength: 0.05,
}
}
pub fn neutral(t: &Theme) -> Self {
Self {
bg_base: t.bg_panel,
accent: t.accent,
radius: radius::MD,
hairline_alpha: 0.0,
gradient_strength: 0.03,
}
}
pub fn bg_top(&self) -> Color {
shift(self.bg_base, self.gradient_strength)
}
pub fn bg_bot(&self) -> Color {
shift(self.bg_base, -self.gradient_strength)
}
}
pub fn panel_signature_painter(
style: PanelStyle,
) -> impl Fn(&mut llimphi_ui::llimphi_raster::vello::Scene, &mut llimphi_ui::llimphi_text::Typesetter, PaintRect)
+ Send
+ Sync
+ 'static {
move |scene, _ts, rect| {
if rect.w <= 0.0 || rect.h <= 0.0 {
return;
}
let x0 = rect.x as f64;
let y0 = rect.y as f64;
let x1 = (rect.x + rect.w) as f64;
let y1 = (rect.y + rect.h) as f64;
let rr = RoundedRect::new(x0, y0, x1, y1, style.radius);
let gradient = Gradient::new_linear(
Point::new(x0, y0),
Point::new(x0, y1),
)
.with_stops([style.bg_top(), style.bg_bot()].as_slice());
scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rr);
if style.hairline_alpha > 0.0 && rect.w > style.radius as f32 * 2.0 + 4.0 {
let hairline_color = with_alpha_mul(style.accent, style.hairline_alpha);
let hairline = KurboRect::new(
x0 + style.radius,
y0,
x1 - style.radius,
y0 + 1.0,
);
scene.fill(Fill::NonZero, Affine::IDENTITY, hairline_color, None, &hairline);
}
}
}
pub fn panel_view<Msg: Clone + 'static>(
children: Vec<View<Msg>>,
style: PanelStyle,
) -> View<Msg> {
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
..Default::default()
})
.paint_with(panel_signature_painter(style))
.radius(style.radius)
.clip(true)
.children(children)
}
pub fn panel_elevated_view<Msg: Clone + 'static>(
children: Vec<View<Msg>>,
style: PanelStyle,
elev: elevation::Elev,
) -> View<Msg> {
let (a, blur, dy) = elev;
let shadow = Shadow {
color: Color::from_rgba8(0, 0, 0, a),
blur,
dx: 0.0,
dy,
spread: 0.0,
};
panel_view(children, style).shadow(shadow)
}
fn shift(c: Color, delta: f32) -> Color {
let [r, g, b, a] = c.components;
AlphaColor::new([
(r + delta).clamp(0.0, 1.0),
(g + delta).clamp(0.0, 1.0),
(b + delta).clamp(0.0, 1.0),
a,
])
}
fn with_alpha_mul(c: Color, mult: f32) -> Color {
let [r, g, b, a] = c.components;
AlphaColor::new([r, g, b, a * mult])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bg_top_is_brighter_than_bg_bot() {
let t = Theme::dark();
let s = PanelStyle::from_theme(&t);
let top = s.bg_top();
let bot = s.bg_bot();
for i in 0..3 {
assert!(top.components[i] >= bot.components[i],
"canal {i}: top {} < bot {}", top.components[i], bot.components[i]);
}
}
#[test]
fn neutral_style_has_no_hairline() {
let t = Theme::dark();
let s = PanelStyle::neutral(&t);
assert_eq!(s.hairline_alpha, 0.0);
}
#[test]
fn shift_clamps_to_unit() {
let c = Color::from_rgba8(250, 250, 250, 255);
let bright = shift(c, 0.5);
assert!(bright.components[0] <= 1.0);
assert!(bright.components[1] <= 1.0);
}
}