1use kas::draw::color::{Rgba, Rgba8Srgb};
9use kas::event::EventState;
10use kas::theme::Background;
11use kas::WidgetId;
12use std::str::FromStr;
13
14const MULT_DEPRESS: f32 = 0.75;
15const MULT_HIGHLIGHT: f32 = 1.25;
16const MIN_HIGHLIGHT: f32 = 0.2;
17
18bitflags::bitflags! {
19 #[derive(Default)]
24 pub struct InputState: u8 {
25 const DISABLED = 1 << 0;
29 const HOVER = 1 << 2;
31 const DEPRESS = 1 << 3;
36 const NAV_FOCUS = 1 << 4;
38 const CHAR_FOCUS = 1 << 5;
41 const SEL_FOCUS = 1 << 6;
44 }
45}
46
47impl InputState {
48 pub fn new_all(ev: &EventState, id: &WidgetId) -> Self {
50 let mut state = Self::new_except_depress(ev, id);
51 if ev.is_depressed(id) {
52 state |= InputState::DEPRESS;
53 }
54 state
55 }
56
57 pub fn new_except_depress(ev: &EventState, id: &WidgetId) -> Self {
59 let (char_focus, sel_focus) = ev.has_char_focus(id);
60 let mut state = InputState::empty();
61 if ev.is_disabled(id) {
62 state |= InputState::DISABLED;
63 }
64 if ev.is_hovered(id) {
65 state |= InputState::HOVER;
66 }
67 if ev.has_nav_focus(id) {
68 state |= InputState::NAV_FOCUS;
69 }
70 if char_focus {
71 state |= InputState::CHAR_FOCUS;
72 }
73 if sel_focus {
74 state |= InputState::SEL_FOCUS;
75 }
76 state
77 }
78
79 pub fn new2(ev: &EventState, id: &WidgetId, id2: &WidgetId) -> Self {
81 let mut state = Self::new_all(ev, id);
82 if ev.is_hovered(id2) {
83 state |= InputState::HOVER;
84 }
85 state
86 }
87
88 #[inline]
90 pub fn disabled(self) -> bool {
91 self.contains(InputState::DISABLED)
92 }
93
94 #[inline]
96 pub fn hover(self) -> bool {
97 self.contains(InputState::HOVER)
98 }
99
100 #[inline]
102 pub fn depress(self) -> bool {
103 self.contains(InputState::DEPRESS)
104 }
105
106 #[inline]
108 pub fn nav_focus(self) -> bool {
109 self.contains(InputState::NAV_FOCUS)
110 }
111
112 #[inline]
114 pub fn char_focus(self) -> bool {
115 self.contains(InputState::CHAR_FOCUS)
116 }
117
118 #[inline]
120 pub fn sel_focus(self) -> bool {
121 self.contains(InputState::SEL_FOCUS)
122 }
123}
124
125#[allow(clippy::derive_partial_eq_without_eq)]
127#[derive(Clone, Debug, PartialEq)]
128#[cfg_attr(feature = "config", derive(serde::Serialize, serde::Deserialize))]
129pub struct Colors<C> {
130 pub is_dark: bool,
132 pub background: C,
134 pub frame: C,
136 pub edit_bg: C,
138 pub edit_bg_disabled: C,
140 pub edit_bg_error: C,
142 pub accent: C,
146 pub accent_soft: C,
150 pub nav_focus: C,
156 pub text: C,
158 pub text_invert: C,
160 pub text_disabled: C,
162 pub text_sel_bg: C,
166}
167
168pub type ColorsSrgb = Colors<Rgba8Srgb>;
170
171pub type ColorsLinear = Colors<Rgba>;
173
174impl From<ColorsSrgb> for ColorsLinear {
175 fn from(col: ColorsSrgb) -> Self {
176 Colors {
177 is_dark: col.is_dark,
178 background: col.background.into(),
179 frame: col.frame.into(),
180 accent: col.accent.into(),
181 accent_soft: col.accent_soft.into(),
182 nav_focus: col.nav_focus.into(),
183 edit_bg: col.edit_bg.into(),
184 edit_bg_disabled: col.edit_bg_disabled.into(),
185 edit_bg_error: col.edit_bg_error.into(),
186 text: col.text.into(),
187 text_invert: col.text_invert.into(),
188 text_disabled: col.text_disabled.into(),
189 text_sel_bg: col.text_sel_bg.into(),
190 }
191 }
192}
193
194impl From<ColorsLinear> for ColorsSrgb {
195 fn from(col: ColorsLinear) -> Self {
196 Colors {
197 is_dark: col.is_dark,
198 background: col.background.into(),
199 frame: col.frame.into(),
200 accent: col.accent.into(),
201 accent_soft: col.accent_soft.into(),
202 nav_focus: col.nav_focus.into(),
203 edit_bg: col.edit_bg.into(),
204 edit_bg_disabled: col.edit_bg_disabled.into(),
205 edit_bg_error: col.edit_bg_error.into(),
206 text: col.text.into(),
207 text_invert: col.text_invert.into(),
208 text_disabled: col.text_disabled.into(),
209 text_sel_bg: col.text_sel_bg.into(),
210 }
211 }
212}
213
214impl Default for ColorsLinear {
215 #[cfg(feature = "dark-light")]
216 fn default() -> Self {
217 match dark_light::detect() {
218 dark_light::Mode::Dark => ColorsSrgb::dark().into(),
219 dark_light::Mode::Light => ColorsSrgb::light().into(),
220 }
221 }
222
223 #[cfg(not(feature = "dark-light"))]
224 #[inline]
225 fn default() -> Self {
226 ColorsSrgb::default().into()
227 }
228}
229
230impl Default for ColorsSrgb {
231 #[inline]
232 fn default() -> Self {
233 ColorsSrgb::light()
234 }
235}
236
237impl ColorsSrgb {
238 pub fn light() -> Self {
240 Colors {
241 is_dark: false,
242 background: Rgba8Srgb::from_str("#FAFAFA").unwrap(),
243 frame: Rgba8Srgb::from_str("#BCBCBC").unwrap(),
244 accent: Rgba8Srgb::from_str("#8347f2").unwrap(),
245 accent_soft: Rgba8Srgb::from_str("#B38DF9").unwrap(),
246 nav_focus: Rgba8Srgb::from_str("#7E3FF2").unwrap(),
247 edit_bg: Rgba8Srgb::from_str("#FAFAFA").unwrap(),
248 edit_bg_disabled: Rgba8Srgb::from_str("#DCDCDC").unwrap(),
249 edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(),
250 text: Rgba8Srgb::from_str("#000000").unwrap(),
251 text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(),
252 text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(),
253 text_sel_bg: Rgba8Srgb::from_str("#A172FA").unwrap(),
254 }
255 }
256
257 pub fn dark() -> Self {
259 Colors {
260 is_dark: true,
261 background: Rgba8Srgb::from_str("#404040").unwrap(),
262 frame: Rgba8Srgb::from_str("#AAAAAA").unwrap(),
263 accent: Rgba8Srgb::from_str("#F74C00").unwrap(),
264 accent_soft: Rgba8Srgb::from_str("#E77346").unwrap(),
265 nav_focus: Rgba8Srgb::from_str("#D03E00").unwrap(),
266 edit_bg: Rgba8Srgb::from_str("#303030").unwrap(),
267 edit_bg_disabled: Rgba8Srgb::from_str("#606060").unwrap(),
268 edit_bg_error: Rgba8Srgb::from_str("#a06868").unwrap(),
269 text: Rgba8Srgb::from_str("#FFFFFF").unwrap(),
270 text_invert: Rgba8Srgb::from_str("#000000").unwrap(),
271 text_disabled: Rgba8Srgb::from_str("#CBCBCB").unwrap(),
272 text_sel_bg: Rgba8Srgb::from_str("#E77346").unwrap(),
273 }
274 }
275
276 pub fn blue() -> Self {
278 Colors {
279 is_dark: false,
280 background: Rgba8Srgb::from_str("#FFFFFF").unwrap(),
281 frame: Rgba8Srgb::from_str("#DADADA").unwrap(),
282 accent: Rgba8Srgb::from_str("#3fafd7").unwrap(),
283 accent_soft: Rgba8Srgb::from_str("#7CDAFF").unwrap(),
284 nav_focus: Rgba8Srgb::from_str("#3B697A").unwrap(),
285 edit_bg: Rgba8Srgb::from_str("#FFFFFF").unwrap(),
286 edit_bg_disabled: Rgba8Srgb::from_str("#DCDCDC").unwrap(),
287 edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(),
288 text: Rgba8Srgb::from_str("#000000").unwrap(),
289 text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(),
290 text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(),
291 text_sel_bg: Rgba8Srgb::from_str("#6CC0E1").unwrap(),
292 }
293 }
294}
295
296impl ColorsLinear {
297 pub fn adjust_for_state(col: Rgba, state: InputState) -> Rgba {
299 if state.disabled() {
300 col.average()
301 } else if state.depress() {
302 col.multiply(MULT_DEPRESS)
303 } else if state.hover() || state.char_focus() {
304 col.multiply(MULT_HIGHLIGHT).max(MIN_HIGHLIGHT)
305 } else {
306 col
307 }
308 }
309
310 pub fn from_bg(&self, bg: Background, state: InputState, force_accent: bool) -> Rgba {
312 let use_accent = force_accent || state.depress() || state.nav_focus();
313 let col = match bg {
314 _ if state.disabled() => self.edit_bg_disabled,
315 Background::Default if use_accent => self.accent_soft,
316 Background::Default => self.background,
317 Background::Error => self.edit_bg_error,
318 Background::Rgb(rgb) => rgb.into(),
319 };
320 Self::adjust_for_state(col, state)
321 }
322
323 pub fn from_edit_bg(&self, bg: Background, state: InputState) -> Rgba {
325 let mut col = match bg {
326 _ if state.disabled() => self.edit_bg_disabled,
327 Background::Default => self.edit_bg,
328 Background::Error => self.edit_bg_error,
329 Background::Rgb(rgb) => rgb.into(),
330 };
331 if state.depress() {
332 col = col.multiply(MULT_DEPRESS);
333 }
334 col
335 }
336
337 pub fn nav_region(&self, state: InputState) -> Option<Rgba> {
339 if state.nav_focus() && !state.disabled() {
340 Some(self.nav_focus)
341 } else {
342 None
343 }
344 }
345
346 #[inline]
348 pub fn accent_state(&self, state: InputState) -> Rgba {
349 Self::adjust_for_state(self.accent, state)
350 }
351
352 #[inline]
354 pub fn accent_soft_state(&self, state: InputState) -> Rgba {
355 Self::adjust_for_state(self.accent_soft, state)
356 }
357
358 #[inline]
360 pub fn check_mark_state(&self, state: InputState) -> Rgba {
361 Self::adjust_for_state(self.accent, state)
362 }
363
364 pub fn menu_entry(&self, state: InputState) -> Option<Rgba> {
366 if state.depress() || state.nav_focus() {
367 Some(self.accent_soft.multiply(MULT_DEPRESS))
368 } else {
369 None
370 }
371 }
372
373 pub fn text_over(&self, bg: Rgba) -> Rgba {
375 let bg_sum = bg.sum();
376 if (bg_sum - self.text_invert.sum()).abs() > (bg_sum - self.text.sum()).abs() {
377 self.text_invert
378 } else {
379 self.text
380 }
381 }
382}