use crate::{Color, ColorState, MotionValue, Transition};
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
pub enum StyleProperty {
RadiusTopLeft,
RadiusTopRight,
RadiusBottomRight,
RadiusBottomLeft,
BorderTop,
BorderRight,
BorderBottom,
BorderLeft,
PaddingTop,
PaddingRight,
PaddingBottom,
PaddingLeft,
MarginTop,
MarginRight,
MarginBottom,
MarginLeft,
GapX,
GapY,
ShadowX,
ShadowY,
ShadowBlur,
ShadowSpread,
Blur,
BackdropBlur,
}
const COUNT: usize = 24;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(usize)]
pub enum ColorProperty {
Background,
Text,
Border,
Shadow,
}
fn signed(index: usize) -> bool {
(12..16).contains(&index) || [18, 19, 21].contains(&index)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct StyleTarget {
values: [Option<f64>; COUNT],
colors: [Option<Color>; 4],
}
impl StyleTarget {
pub fn value(mut self, property: StyleProperty, value: f32) -> Self {
let index = property as usize;
assert!(
value.is_finite() && (signed(index) || value >= 0.),
"invalid style value"
);
self.values[index] = Some(value.into());
self
}
pub fn color(mut self, property: ColorProperty, color: Color) -> Self {
self.colors[property as usize] = Some(color);
self
}
pub fn get(self, property: StyleProperty) -> Option<f32> {
let index = property as usize;
self.values[index].map(|v| {
v.clamp(
if signed(index) {
f64::from(f32::MIN)
} else {
0.
},
f64::from(f32::MAX),
) as f32
})
}
pub fn get_color(self, property: ColorProperty) -> Option<Color> {
self.colors[property as usize]
}
pub fn radii(mut self, values: [f32; 4]) -> Self {
for (property, value) in [
StyleProperty::RadiusTopLeft,
StyleProperty::RadiusTopRight,
StyleProperty::RadiusBottomRight,
StyleProperty::RadiusBottomLeft,
]
.into_iter()
.zip(values)
{
self = self.value(property, value);
}
self
}
pub fn border_widths(mut self, values: [f32; 4]) -> Self {
for (property, value) in [
StyleProperty::BorderTop,
StyleProperty::BorderRight,
StyleProperty::BorderBottom,
StyleProperty::BorderLeft,
]
.into_iter()
.zip(values)
{
self = self.value(property, value);
}
self
}
pub fn padding(mut self, values: [f32; 4]) -> Self {
for (property, value) in [
StyleProperty::PaddingTop,
StyleProperty::PaddingRight,
StyleProperty::PaddingBottom,
StyleProperty::PaddingLeft,
]
.into_iter()
.zip(values)
{
self = self.value(property, value);
}
self
}
pub fn margin(mut self, values: [f32; 4]) -> Self {
for (property, value) in [
StyleProperty::MarginTop,
StyleProperty::MarginRight,
StyleProperty::MarginBottom,
StyleProperty::MarginLeft,
]
.into_iter()
.zip(values)
{
self = self.value(property, value);
}
self
}
pub fn gap(self, x: f32, y: f32) -> Self {
self.value(StyleProperty::GapX, x)
.value(StyleProperty::GapY, y)
}
pub fn shadow(self, offset: [f32; 2], blur: f32, spread: f32, color: Color) -> Self {
self.value(StyleProperty::ShadowX, offset[0])
.value(StyleProperty::ShadowY, offset[1])
.value(StyleProperty::ShadowBlur, blur)
.value(StyleProperty::ShadowSpread, spread)
.color(ColorProperty::Shadow, color)
}
}
#[derive(Clone, Copy, Debug)]
pub struct StyleMotion {
target: StyleTarget,
initial: Option<StyleTarget>,
transition: Transition,
overrides: [Option<Transition>; COUNT],
color_overrides: [Option<Transition>; 4],
}
impl StyleMotion {
pub fn new(target: StyleTarget) -> Self {
Self {
target,
initial: None,
transition: Transition::default(),
overrides: [None; COUNT],
color_overrides: [None; 4],
}
}
pub fn initial(mut self, target: StyleTarget) -> Self {
self.initial = Some(target);
self
}
pub fn transition(mut self, transition: Transition) -> Self {
self.transition = transition;
self
}
pub fn transition_for(mut self, property: StyleProperty, transition: Transition) -> Self {
self.overrides[property as usize] = Some(transition);
self
}
pub fn color_transition(mut self, property: ColorProperty, transition: Transition) -> Self {
self.color_overrides[property as usize] = Some(transition);
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct StyleFrame {
target: StyleTarget,
active: bool,
completed: bool,
}
impl StyleFrame {
pub fn target(self) -> StyleTarget {
self.target
}
pub fn is_animating(self) -> bool {
self.active
}
pub fn just_completed(self) -> bool {
self.completed
}
}
#[derive(Debug, Default)]
pub struct StyleMotionState {
values: [Option<MotionValue>; COUNT],
colors: [Option<ColorState>; 4],
destination: StyleTarget,
initialized: bool,
active: bool,
#[cfg(feature = "gpui")]
epoch: Option<std::time::Instant>,
}
impl StyleMotionState {
pub fn sample(&mut self, motion: StyleMotion, now: Duration, reduced: bool) -> StyleFrame {
let mut result = StyleTarget::default();
let mut active = false;
let mut completed = false;
for i in 0..COUNT {
if let Some(value) = motion.target.values[i] {
self.destination.values[i] = Some(value);
}
if let Some(target) = self.destination.values[i] {
let initial = if self.initialized {
target
} else {
motion.initial.and_then(|v| v.values[i]).unwrap_or(target)
};
let state = self.values[i].get_or_insert_with(|| MotionValue::new(initial));
state.animate(
target,
motion.overrides[i].unwrap_or(motion.transition),
now,
);
let frame = state.sample(now, reduced);
completed |= frame.just_completed();
result.values[i] = Some(frame.value());
active |= frame.is_animating();
}
}
for i in 0..4 {
if let Some(color) = motion.target.colors[i] {
completed |= self.colors[i].is_some() && self.destination.colors[i] != Some(color);
self.destination.colors[i] = Some(color);
}
if let Some(target) = self.destination.colors[i] {
let initial = if self.initialized {
target
} else {
motion.initial.and_then(|v| v.colors[i]).unwrap_or(target)
};
completed |= !self.initialized && initial != target;
let state = self.colors[i].get_or_insert_with(|| ColorState::new(initial));
let (color, running) = state.sample(
target,
motion.color_overrides[i].unwrap_or(motion.transition),
now,
reduced,
);
result.colors[i] = Some(color);
active |= running;
}
}
let completed = (self.active || completed) && !active;
self.active = active;
self.initialized = true;
StyleFrame {
target: result,
active,
completed,
}
}
#[cfg(feature = "gpui")]
pub fn frame(
&mut self,
motion: StyleMotion,
window: &mut gpui::Window,
cx: &gpui::App,
) -> StyleFrame {
let now = *self.epoch.get_or_insert_with(std::time::Instant::now);
let frame = self.sample(motion, now.elapsed(), cx.reduce_motion());
if frame.is_animating() {
window.request_animation_frame();
}
frame
}
}
#[cfg(feature = "gpui")]
impl StyleFrame {
pub fn apply<E: gpui::Styled>(self, mut element: E) -> E {
use StyleProperty::*;
use gpui::px;
if let Some(v) = self.target.get(RadiusTopLeft) {
element.style().corner_radii.top_left = Some(px(v).into());
}
if let Some(v) = self.target.get(RadiusTopRight) {
element.style().corner_radii.top_right = Some(px(v).into());
}
if let Some(v) = self.target.get(RadiusBottomRight) {
element.style().corner_radii.bottom_right = Some(px(v).into());
}
if let Some(v) = self.target.get(RadiusBottomLeft) {
element.style().corner_radii.bottom_left = Some(px(v).into());
}
if let Some(v) = self.target.get(BorderTop) {
element.style().border_widths.top = Some(px(v).into());
}
if let Some(v) = self.target.get(BorderRight) {
element.style().border_widths.right = Some(px(v).into());
}
if let Some(v) = self.target.get(BorderBottom) {
element.style().border_widths.bottom = Some(px(v).into());
}
if let Some(v) = self.target.get(BorderLeft) {
element.style().border_widths.left = Some(px(v).into());
}
if let Some(v) = self.target.get(PaddingTop) {
element.style().padding.top = Some(px(v).into());
}
if let Some(v) = self.target.get(PaddingRight) {
element.style().padding.right = Some(px(v).into());
}
if let Some(v) = self.target.get(PaddingBottom) {
element.style().padding.bottom = Some(px(v).into());
}
if let Some(v) = self.target.get(PaddingLeft) {
element.style().padding.left = Some(px(v).into());
}
if let Some(v) = self.target.get(MarginTop) {
element.style().margin.top = Some(px(v).into());
}
if let Some(v) = self.target.get(MarginRight) {
element.style().margin.right = Some(px(v).into());
}
if let Some(v) = self.target.get(MarginBottom) {
element.style().margin.bottom = Some(px(v).into());
}
if let Some(v) = self.target.get(MarginLeft) {
element.style().margin.left = Some(px(v).into());
}
if let Some(v) = self.target.get(GapX) {
element.style().gap.width = Some(px(v).into());
}
if let Some(v) = self.target.get(GapY) {
element.style().gap.height = Some(px(v).into());
}
if let Some(color) = self.target.get_color(ColorProperty::Background) {
element = element.bg(color.to_rgba());
}
if let Some(color) = self.target.get_color(ColorProperty::Text) {
element = element.text_color(color.to_rgba());
}
if let Some(color) = self.target.get_color(ColorProperty::Border) {
element = element.border_color(color.to_rgba());
}
if self.target.values[18..22].iter().any(Option::is_some) || self.target.colors[3].is_some()
{
let color = self
.target
.get_color(ColorProperty::Shadow)
.unwrap_or(Color::rgba(0., 0., 0., 0.25))
.to_rgba();
element.style().box_shadow = Some(vec![
gpui::BoxShadow::new(
px(self.target.get(ShadowX).unwrap_or(0.)),
px(self.target.get(ShadowY).unwrap_or(0.)),
gpui::rgb_to_hsla(color),
)
.blur_radius(px(self.target.get(ShadowBlur).unwrap_or(0.)))
.spread_radius(px(self.target.get(ShadowSpread).unwrap_or(0.))),
]);
}
if let Some(radius) = self.target.get(Blur) {
element = element.filter(vec![gpui::Filter::Blur(px(radius))]);
}
if let Some(radius) = self.target.get(BackdropBlur) {
element = element.backdrop_filter(vec![gpui::Filter::Blur(px(radius))]);
}
element
}
}
impl crate::MotionData<40> for StyleTarget {
fn channels(self) -> [f64; 40] {
std::array::from_fn(|i| {
if i < COUNT {
self.values[i].unwrap_or(0.0)
} else {
self.colors[(i - COUNT) / 4].map_or(0.0, |color| {
crate::MotionData::<4>::channels(color)[(i - COUNT) % 4]
})
}
})
}
fn with_channels(self, values: [f64; 40]) -> Self {
Self {
values: std::array::from_fn(|i| {
self.values[i].map(|_| {
if signed(i) {
values[i]
} else {
values[i].max(0.0)
}
})
}),
colors: std::array::from_fn(|i| {
self.colors[i].map(|color| {
crate::MotionData::<4>::with_channels(
color,
std::array::from_fn(|j| values[COUNT + i * 4 + j]),
)
})
}),
}
}
fn compatible(self, other: Self) -> bool {
self.values.map(|v| v.is_some()) == other.values.map(|v| v.is_some())
&& self.colors.map(|v| v.is_some()) == other.colors.map(|v| v.is_some())
}
}