1use edit::EditProject;
14use edit::style::{BackgroundConfig, BackgroundSource};
15use leptos::prelude::*;
16
17type ProjectSignal = Option<RwSignal<Option<EditProject>>>;
18type HistoryStore = Option<StoredValue<Option<edit::History>>>;
19
20#[must_use]
22pub fn background_presets() -> Vec<(&'static str, BackgroundSource)> {
23 vec![
24 ("Aurora", BackgroundSource::default()),
25 (
26 "Ocean",
27 BackgroundSource::Gradient {
28 from: [33, 147, 176],
29 to: [109, 213, 237],
30 angle_deg: 135.0,
31 },
32 ),
33 (
34 "Sunset",
35 BackgroundSource::Gradient {
36 from: [255, 126, 95],
37 to: [254, 180, 123],
38 angle_deg: 135.0,
39 },
40 ),
41 ("Graphite", BackgroundSource::Color { rgb: [38, 40, 46] }),
42 (
43 "Snow",
44 BackgroundSource::Color {
45 rgb: [240, 240, 242],
46 },
47 ),
48 ]
49}
50
51#[must_use]
54pub fn source_css(source: &BackgroundSource) -> String {
55 match source {
56 BackgroundSource::Gradient {
57 from,
58 to,
59 angle_deg,
60 } => format!(
61 "linear-gradient({angle_deg}deg, rgb({},{},{}), rgb({},{},{}))",
62 from[0], from[1], from[2], to[0], to[1], to[2]
63 ),
64 BackgroundSource::Color { rgb } => format!("rgb({},{},{})", rgb[0], rgb[1], rgb[2]),
65 BackgroundSource::Wallpaper { .. } => "#33363d".to_owned(),
67 }
68}
69
70#[must_use]
72pub fn is_active_source(current: &BackgroundSource, preset: &BackgroundSource) -> bool {
73 current == preset
74}
75
76#[must_use]
78pub fn parse_u32_field(s: &str, max: u32) -> u32 {
79 s.trim().parse::<u32>().unwrap_or(0).min(max)
80}
81
82fn swatches(project: ProjectSignal, history: HistoryStore) -> AnyView {
83 let current = project
84 .and_then(|s| s.get().map(|p| p.background.source))
85 .unwrap_or_default();
86 background_presets()
87 .into_iter()
88 .map(|(name, source)| {
89 let mut class = String::from("style-swatch");
90 if is_active_source(¤t, &source) {
91 class.push_str(" style-swatch--active");
92 }
93 let css = source_css(&source);
94 view! {
95 <button
96 class=class
97 title=name
98 style=format!("background:{css}")
99 on:click=move |_| {
100 if let (Some(p), Some(h)) = (project, history) {
101 let mut cfg = p
102 .get_untracked()
103 .map(|pr| pr.background)
104 .unwrap_or_default();
105 cfg.source = source.clone();
106 crate::editor_edits::set_background(p, h, cfg);
107 }
108 }
109 ></button>
110 }
111 })
112 .collect_view()
113 .into_any()
114}
115
116fn number_field(
118 project: ProjectSignal,
119 history: HistoryStore,
120 label: &'static str,
121 value: u32,
122 max: u32,
123 set: fn(&mut BackgroundConfig, u32),
124) -> AnyView {
125 view! {
126 <label class="style-field">
127 <span class="style-field-label">{label}</span>
128 <input
129 class="style-field-input"
130 type="number"
131 min="0"
132 max=max.to_string()
133 prop:value=value.to_string()
134 on:change=move |ev| {
135 if let (Some(p), Some(h)) = (project, history) {
136 let mut cfg = p.get_untracked().map(|pr| pr.background).unwrap_or_default();
137 set(&mut cfg, parse_u32_field(&event_target_value(&ev), max));
138 crate::editor_edits::set_background(p, h, cfg);
139 }
140 }
141 />
142 </label>
143 }
144 .into_any()
145}
146
147#[component]
149pub fn StyleInspector() -> impl IntoView {
150 let project = use_context::<RwSignal<Option<EditProject>>>();
151 let history = use_context::<StoredValue<Option<edit::History>>>();
152 view! {
153 <div class="style-inspector">
154 <div class="clip-inspector-section">
155 <h3 class="clip-inspector-title">"Background"</h3>
156 <div class="style-swatches">{move || swatches(project, history)}</div>
157 </div>
158 <div class="clip-inspector-section">
159 <h3 class="clip-inspector-title">"Framing"</h3>
160 <div class="style-fields">
161 {move || {
162 let bg = project
163 .and_then(|s| s.get().map(|p| p.background))
164 .unwrap_or_default();
165 view! {
166 {number_field(project, history, "Padding", bg.padding, 400, |c, v| c.padding = v)}
167 {number_field(project, history, "Radius", bg.corner_radius, 200, |c, v| c.corner_radius = v)}
168 {number_field(project, history, "Shadow", bg.shadow, 100, |c, v| c.shadow = v)}
169 }
170 }}
171 </div>
172 </div>
173 </div>
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn presets_include_default_and_a_color() {
183 let p = background_presets();
184 assert!(p.len() >= 4);
185 assert!(p.iter().any(|(n, _)| *n == "Aurora"));
186 assert!(
187 p.iter()
188 .any(|(_, s)| matches!(s, BackgroundSource::Color { .. }))
189 );
190 }
191
192 #[test]
193 fn source_css_renders_gradient_and_color() {
194 let g = source_css(&BackgroundSource::Gradient {
195 from: [1, 2, 3],
196 to: [4, 5, 6],
197 angle_deg: 90.0,
198 });
199 assert_eq!(g, "linear-gradient(90deg, rgb(1,2,3), rgb(4,5,6))");
200 assert_eq!(
201 source_css(&BackgroundSource::Color { rgb: [10, 20, 30] }),
202 "rgb(10,20,30)"
203 );
204 }
205
206 #[test]
207 fn parse_u32_clamps_and_defaults() {
208 assert_eq!(parse_u32_field("64", 400), 64);
209 assert_eq!(parse_u32_field("999", 400), 400); assert_eq!(parse_u32_field("abc", 400), 0); assert_eq!(parse_u32_field("-5", 400), 0); }
213
214 #[test]
215 fn active_source_matches_equal_presets() {
216 let a = BackgroundSource::Color { rgb: [1, 1, 1] };
217 let b = BackgroundSource::Color { rgb: [1, 1, 1] };
218 let c = BackgroundSource::Color { rgb: [2, 2, 2] };
219 assert!(is_active_source(&a, &b));
220 assert!(!is_active_source(&a, &c));
221 }
222}