Color scheme and some ui cleanup

main
Elijah Voigt 3 weeks ago
parent 6b492c9a62
commit 97aa5cb20d

@ -0,0 +1,411 @@
//! Theme color generator for Bevy Feathers.
//!
//! # Main entrypoint
//!
//! [`build`] takes [`Seeds`] and a [`Mode`] and returns a complete Feathers
//! [`ThemeProps`](bevy::feathers::theme::ThemeProps).
//!
//! ```rust,ignore
//! // Auto-assign roles from a 5-swatch Coolors palette (ranked by chroma).
//! let seeds = Seeds::from_coolors([color1, color2, color3, color4, color5]);
//! let theme = build(seeds, Mode::Dark);
//! // Hand `theme` to Feathers.
//! ```
//!
//! Construct [`Seeds`] directly if you need manual role assignment.
use bevy::color::{Alpha, Color, Oklcha};
use bevy::feathers::theme::{ThemeProps, ThemeToken};
use bevy::feathers::tokens as ft;
use bevy::platform::collections::HashMap;
// ---------------------------------------------------------------------------
// Color math
// ---------------------------------------------------------------------------
fn relative_luminance(c: Color) -> f32 {
let lin = c.to_linear();
0.2126 * lin.red + 0.7152 * lin.green + 0.0722 * lin.blue
}
fn contrast_ratio(a: Color, b: Color) -> f32 {
let (la, lb) = (relative_luminance(a), relative_luminance(b));
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
(hi + 0.05) / (lo + 0.05)
}
/// Pick the softest foreground at the given hue/chroma that meets `target`
/// WCAG contrast on `bg`. Falls back to pure white/black if nothing passes.
fn on_color(bg: Color, hue: f32, chroma: f32, target: f32) -> Color {
let bg_lum = relative_luminance(bg);
let mut best: Option<(f32, Color)> = None;
for i in 0..=100 {
let l = i as f32 / 100.0;
let cand = Color::oklcha(l, chroma, hue, 1.0);
if contrast_ratio(cand, bg) >= target {
let d = (relative_luminance(cand) - bg_lum).abs();
if best.map_or(true, |(bd, _)| d < bd) {
best = Some((d, cand));
}
}
}
best.map(|(_, c)| c)
.unwrap_or_else(|| Color::oklcha(if bg_lum < 0.5 { 1.0 } else { 0.0 }, 0.0, hue, 1.0))
}
/// Lighter in dark mode, darker in light mode — keeps hue/chroma.
fn elevate(c: Color, mode: Mode, amount: f32) -> Color {
let o = Oklcha::from(c);
let l = match mode {
Mode::Dark => o.lightness + amount,
Mode::Light => o.lightness - amount,
};
Color::from(o.with_lightness(l.clamp(0.0, 1.0)))
}
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Mode {
Dark,
Light,
}
/// The three seed colors the generator needs. Use `from_coolors` to
/// auto-assign roles from a 5-swatch palette.
#[derive(Clone, Copy, Debug)]
pub struct Seeds {
pub primary: Color,
pub secondary: Color,
pub accent: Color,
}
impl Seeds {
/// Assign roles from an unordered 5-swatch palette (e.g. from Coolors).
/// Most-chromatic swatch becomes accent, next primary, next secondary.
pub fn from_coolors(palette: [Color; 5]) -> Self {
let mut by_chroma: Vec<Oklcha> = palette.iter().map(|c| Oklcha::from(*c)).collect();
by_chroma.sort_by(|a, b| b.chroma.total_cmp(&a.chroma));
let pick = |o: Oklcha| Color::from(o);
Seeds {
accent: pick(by_chroma[0]),
primary: pick(by_chroma[1]),
secondary: pick(by_chroma[2]),
}
}
}
// ---------------------------------------------------------------------------
// Lightness anchors
// ---------------------------------------------------------------------------
struct Anchors {
window: f32,
sunken: f32,
surface: f32,
raised: f32,
border: f32,
control: f32,
text: f32,
text_dim: f32,
text_disabled: f32,
accent: f32,
status: f32,
}
impl Anchors {
fn for_mode(mode: Mode) -> Self {
match mode {
Mode::Dark => Anchors {
window: 0.15,
sunken: 0.12,
surface: 0.19,
raised: 0.24,
border: 0.32,
control: 0.27,
text: 0.96,
text_dim: 0.74,
text_disabled: 0.46,
accent: 0.62,
status: 0.62,
},
Mode::Light => Anchors {
window: 0.965,
sunken: 0.93,
surface: 0.99,
raised: 1.00,
border: 0.85,
control: 0.96,
text: 0.22,
text_dim: 0.45,
text_disabled: 0.70,
accent: 0.55,
status: 0.52,
},
}
}
}
// ---------------------------------------------------------------------------
// Theme builder
// ---------------------------------------------------------------------------
/// Build a complete Feathers `ThemeProps` from seed colors.
///
/// All colors are derived in OKLCH so hue never drifts, neutrals carry a
/// whisper of the primary hue, and text colors are WCAG-contrast-checked.
pub fn build(seeds: Seeds, mode: Mode) -> ThemeProps {
let a = Anchors::for_mode(mode);
let primary = Oklcha::from(seeds.primary);
let accent = Oklcha::from(seeds.accent);
let neutral_hue = primary.hue;
let neutral = |l: f32| Color::oklcha(l, 0.012, neutral_hue, 1.0);
let shade = |seed: Oklcha, l: f32| Color::oklcha(l, seed.chroma, seed.hue, 1.0);
let status_chroma = ((primary.chroma + accent.chroma) * 0.5).clamp(0.08, 0.16);
let status = |hue: f32| Color::oklcha(a.status, status_chroma, hue, 1.0);
let control = neutral(a.control);
let sunken = neutral(a.sunken);
let border = neutral(a.border);
let text = neutral(a.text);
let text_dim = neutral(a.text_dim);
let text_disabled = neutral(a.text_disabled);
let accent_bg = shade(accent, a.accent);
let accent_hover = elevate(accent_bg, mode, 0.05);
let accent_pressed = elevate(accent_bg, mode, 0.10);
let focus_ring = shade(accent, if mode == Mode::Dark { 0.70 } else { 0.55 });
let btn_text = on_color(control, neutral_hue, 0.0, 4.5);
let accent_text = on_color(accent_bg, accent.hue, 0.03, 4.5);
let mut colors: HashMap<ThemeToken, Color> = HashMap::new();
macro_rules! set {
($tok:expr, $color:expr) => {
colors.insert($tok.clone(), $color)
};
}
// Window / pane surfaces
set!(ft::WINDOW_BG, neutral(a.window));
set!(ft::PANE_HEADER_BG, neutral(a.window));
set!(ft::PANE_HEADER_BORDER, border);
set!(ft::PANE_HEADER_TEXT, text);
set!(ft::PANE_HEADER_DIVIDER, border);
set!(ft::PANE_BODY_BG, neutral(a.surface));
set!(ft::SUBPANE_HEADER_BG, neutral(a.raised));
set!(ft::SUBPANE_HEADER_BORDER, border);
set!(ft::SUBPANE_HEADER_TEXT, text);
set!(ft::SUBPANE_BODY_BG, neutral(a.surface));
set!(ft::SUBPANE_BODY_BORDER, border);
set!(ft::GROUP_HEADER_BG, neutral(a.raised));
set!(ft::GROUP_HEADER_BORDER, border);
set!(ft::GROUP_HEADER_TEXT, text);
set!(ft::GROUP_BODY_BG, neutral(a.raised));
set!(ft::GROUP_BODY_BORDER, border);
// Focus ring
set!(ft::FOCUS_RING, focus_ring);
// Text
set!(ft::TEXT_MAIN, text);
set!(ft::TEXT_DIM, text_dim);
// Normal buttons
set!(ft::BUTTON_BG, control);
set!(ft::BUTTON_BG_HOVER, elevate(control, mode, 0.04));
set!(ft::BUTTON_BG_PRESSED, elevate(control, mode, 0.08));
set!(ft::BUTTON_BG_DISABLED, control.with_alpha(0.4));
set!(ft::BUTTON_TEXT, btn_text);
set!(ft::BUTTON_TEXT_DISABLED, btn_text.with_alpha(0.5));
// Primary (accent) buttons
set!(ft::BUTTON_PRIMARY_BG, accent_bg);
set!(ft::BUTTON_PRIMARY_BG_HOVER, accent_hover);
set!(ft::BUTTON_PRIMARY_BG_PRESSED, accent_pressed);
set!(ft::BUTTON_PRIMARY_BG_DISABLED, accent_bg.with_alpha(0.4));
set!(ft::BUTTON_PRIMARY_TEXT, accent_text);
set!(
ft::BUTTON_PRIMARY_TEXT_DISABLED,
accent_text.with_alpha(0.5)
);
// Plain (transparent) buttons
set!(ft::BUTTON_PLAIN_BG, Color::NONE);
set!(ft::BUTTON_PLAIN_BG_HOVER, neutral(a.raised));
set!(ft::BUTTON_PLAIN_BG_PRESSED, control);
set!(ft::BUTTON_PLAIN_BG_DISABLED, Color::NONE);
// Sliders
set!(ft::SLIDER_BG, sunken);
set!(ft::SLIDER_BG_HOVER, elevate(sunken, mode, 0.03));
set!(ft::SLIDER_BG_PRESSED, elevate(sunken, mode, 0.06));
set!(ft::SLIDER_BG_DISABLED, sunken.with_alpha(0.5));
set!(ft::SLIDER_BAR, accent_bg);
set!(ft::SLIDER_BAR_HOVER, accent_hover);
set!(ft::SLIDER_BAR_PRESSED, accent_pressed);
set!(ft::SLIDER_BAR_DISABLED, accent_bg.with_alpha(0.5));
set!(ft::SLIDER_TEXT, on_color(sunken, neutral_hue, 0.0, 4.5));
set!(ft::SLIDER_TEXT_DISABLED, text_disabled.with_alpha(0.5));
// Scrollbar
set!(ft::SCROLLBAR_BG, sunken);
set!(ft::SCROLLBAR_THUMB, accent_bg);
set!(ft::SCROLLBAR_THUMB_HOVER, accent_hover);
// Checkboxes
set!(ft::CHECKBOX_BG, control);
set!(ft::CHECKBOX_BG_HOVER, elevate(control, mode, 0.04));
set!(ft::CHECKBOX_BG_PRESSED, elevate(control, mode, 0.08));
set!(ft::CHECKBOX_BG_DISABLED, control.with_alpha(0.4));
set!(ft::CHECKBOX_BG_CHECKED, accent_bg);
set!(ft::CHECKBOX_BG_CHECKED_HOVER, accent_hover);
set!(ft::CHECKBOX_BG_CHECKED_PRESSED, accent_pressed);
set!(ft::CHECKBOX_BG_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
set!(ft::CHECKBOX_BORDER, border);
set!(ft::CHECKBOX_BORDER_HOVER, elevate(border, mode, 0.04));
set!(ft::CHECKBOX_BORDER_PRESSED, elevate(border, mode, 0.08));
set!(ft::CHECKBOX_BORDER_DISABLED, border.with_alpha(0.4));
set!(ft::CHECKBOX_BORDER_CHECKED, accent_bg);
set!(ft::CHECKBOX_BORDER_CHECKED_HOVER, accent_hover);
set!(ft::CHECKBOX_BORDER_CHECKED_PRESSED, accent_pressed);
set!(
ft::CHECKBOX_BORDER_CHECKED_DISABLED,
accent_bg.with_alpha(0.4)
);
set!(ft::CHECKBOX_MARK, accent_text);
set!(ft::CHECKBOX_MARK_DISABLED, text_disabled);
set!(ft::CHECKBOX_TEXT, text);
set!(ft::CHECKBOX_TEXT_DISABLED, text_disabled.with_alpha(0.5));
// Radio buttons
set!(ft::RADIO_BORDER, border);
set!(ft::RADIO_BORDER_HOVER, elevate(border, mode, 0.04));
set!(ft::RADIO_BORDER_PRESSED, elevate(border, mode, 0.08));
set!(ft::RADIO_BORDER_DISABLED, border.with_alpha(0.4));
set!(ft::RADIO_BORDER_CHECKED, accent_bg);
set!(ft::RADIO_BORDER_CHECKED_HOVER, accent_hover);
set!(ft::RADIO_BORDER_CHECKED_PRESSED, accent_pressed);
set!(ft::RADIO_BORDER_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
set!(ft::RADIO_MARK, accent_bg);
set!(ft::RADIO_MARK_HOVER, accent_hover);
set!(ft::RADIO_MARK_PRESSED, accent_pressed);
set!(ft::RADIO_MARK_DISABLED, border.with_alpha(0.4));
set!(ft::RADIO_TEXT, text);
set!(ft::RADIO_TEXT_DISABLED, text_disabled.with_alpha(0.5));
// Toggle switches
let slide = text; // high-contrast nub that slides
set!(ft::SWITCH_BG, control);
set!(ft::SWITCH_BG_HOVER, elevate(control, mode, 0.04));
set!(ft::SWITCH_BG_PRESSED, elevate(control, mode, 0.08));
set!(ft::SWITCH_BG_DISABLED, control.with_alpha(0.4));
set!(ft::SWITCH_BG_CHECKED, accent_bg);
set!(ft::SWITCH_BG_CHECKED_HOVER, accent_hover);
set!(ft::SWITCH_BG_CHECKED_PRESSED, accent_pressed);
set!(ft::SWITCH_BG_CHECKED_DISABLED, accent_bg.with_alpha(0.4));
set!(ft::SWITCH_BORDER, border);
set!(ft::SWITCH_BORDER_HOVER, elevate(border, mode, 0.04));
set!(ft::SWITCH_BORDER_PRESSED, elevate(border, mode, 0.08));
set!(ft::SWITCH_BORDER_DISABLED, border.with_alpha(0.4));
set!(ft::SWITCH_BORDER_CHECKED, accent_bg);
set!(ft::SWITCH_BORDER_CHECKED_HOVER, accent_hover);
set!(ft::SWITCH_BORDER_CHECKED_PRESSED, accent_pressed);
set!(
ft::SWITCH_BORDER_CHECKED_DISABLED,
accent_bg.with_alpha(0.4)
);
set!(ft::SWITCH_SLIDE_BG, slide);
set!(ft::SWITCH_SLIDE_BG_HOVER, slide);
set!(ft::SWITCH_SLIDE_BG_PRESSED, slide);
set!(ft::SWITCH_SLIDE_BG_DISABLED, text_disabled);
set!(ft::SWITCH_SLIDE_BG_CHECKED, slide);
set!(ft::SWITCH_SLIDE_BG_CHECKED_HOVER, slide);
set!(ft::SWITCH_SLIDE_BG_CHECKED_PRESSED, slide);
set!(ft::SWITCH_SLIDE_BG_CHECKED_DISABLED, text_disabled);
set!(ft::SWITCH_SLIDE_BORDER, slide);
set!(ft::SWITCH_SLIDE_BORDER_HOVER, slide);
set!(ft::SWITCH_SLIDE_BORDER_PRESSED, slide);
set!(ft::SWITCH_SLIDE_BORDER_DISABLED, text_disabled);
set!(ft::SWITCH_SLIDE_BORDER_CHECKED, slide);
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_HOVER, slide);
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_PRESSED, slide);
set!(ft::SWITCH_SLIDE_BORDER_CHECKED_DISABLED, text_disabled);
// Color plane
set!(ft::COLOR_PLANE_BG, sunken);
// Menus
let menu_bg = neutral(a.surface);
set!(ft::MENU_BG, menu_bg);
set!(ft::MENU_BORDER, border);
set!(ft::MENUITEM_BG_HOVER, elevate(menu_bg, mode, 0.03));
set!(ft::MENUITEM_BG_PRESSED, elevate(menu_bg, mode, 0.06));
set!(ft::MENUITEM_BG_FOCUSED, elevate(menu_bg, mode, 0.06));
set!(ft::MENUITEM_TEXT, text);
set!(ft::MENUITEM_TEXT_DISABLED, text_disabled.with_alpha(0.5));
// Text input
set!(ft::TEXT_INPUT_BG, sunken);
set!(ft::TEXT_INPUT_LABEL_BG, control);
set!(ft::TEXT_INPUT_TEXT, text);
set!(ft::TEXT_INPUT_TEXT_DISABLED, text_disabled.with_alpha(0.5));
set!(ft::TEXT_INPUT_CURSOR, focus_ring);
set!(ft::TEXT_INPUT_SELECTION, accent_bg.with_alpha(0.5));
set!(ft::TEXT_INPUT_SELECTION_UNFOCUSED, Color::NONE);
set!(ft::TEXT_INPUT_X_AXIS, status(27.0));
set!(ft::TEXT_INPUT_Y_AXIS, status(150.0));
set!(ft::TEXT_INPUT_Z_AXIS, status(270.0));
// Listview
set!(ft::LISTROW_BG, Color::NONE);
set!(ft::LISTROW_BG_HOVER, control.with_alpha(0.4));
set!(ft::LISTROW_BG_SELECTED, control);
set!(ft::LISTROW_TEXT, text);
set!(ft::LISTROW_TEXT_DISABLED, text_disabled.with_alpha(0.5));
ThemeProps { color: colors }
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use bevy::color::Srgba;
use bevy::feathers::tokens as ft;
fn hex(s: &str) -> Color {
Color::from(Srgba::hex(s).unwrap())
}
fn demo_seeds() -> Seeds {
Seeds::from_coolors([
hex("264653"),
hex("2A9D8F"),
hex("E9C46A"),
hex("F4A261"),
hex("E76F51"),
])
}
#[test]
fn text_meets_contrast_in_both_modes() {
for mode in [Mode::Dark, Mode::Light] {
let t = build(demo_seeds(), mode);
let get = |tok: &ThemeToken| *t.color.get(tok).unwrap();
assert!(contrast_ratio(get(&ft::TEXT_MAIN), get(&ft::WINDOW_BG)) >= 4.5);
assert!(
contrast_ratio(get(&ft::BUTTON_PRIMARY_TEXT), get(&ft::BUTTON_PRIMARY_BG)) >= 4.5
);
assert!(contrast_ratio(get(&ft::BUTTON_TEXT), get(&ft::BUTTON_BG)) >= 4.5);
}
}
}

@ -1,3 +1,6 @@
pub mod color;
pub use color::*;
pub use std::time::Duration;
pub use bevy::{
@ -8,7 +11,7 @@ pub use bevy::{
FeathersPlugins,
controls::*,
dark_theme::create_dark_theme,
theme::{ThemedText, UiTheme},
theme::{ThemeProps, ThemedText, UiTheme},
},
input::{
common_conditions::*,

@ -1,4 +1,8 @@
use bevy::{feathers::theme::ThemeProps, ui::InteractionDisabled};
use bevy::{
feathers::{containers::*, display::*, theme::ThemeProps},
ui::{Checked, InteractionDisabled},
ui_widgets::{ActivateOnPress, ValueChange, checkbox_self_update, SliderStep, SliderPrecision, slider_self_update},
};
use engine::*;
const TILE: &str = "tileGrey_01.png";
@ -52,6 +56,7 @@ fn main() {
toggle_interactive::<SimulationPlayback>,
// TODO Visualize simulation run/pause buttons enable/disable
toggle_interactive::<AudioPlayback>,
update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>),
),
)
.add_systems(Update, cell_lifecycle.run_if(on_message::<Lifecycle>))
@ -69,7 +74,7 @@ enum AudioPlayback {
Play,
}
#[derive(Debug, Component, PartialEq, Default)]
#[derive(Debug, Component, PartialEq, Default, Clone, FromTemplate)]
#[require(Visibility, Transform)]
enum UiButton {
Power,
@ -109,6 +114,13 @@ impl UiButton {
}
}
}
fn ui_checkbox(text: &str) -> impl Scene {
bsn! {
@FeathersCheckbox { @caption: bsn! { Text(text) ThemedText } }
ActivateOnPress
on(checkbox_self_update)
}
}
fn ui_button(button: UiButton) -> impl Scene {
bsn! {
@ -130,8 +142,6 @@ fn ui_button(button: UiButton) -> impl Scene {
image: HandleTemplate::Path(AssetPath::parse(button.icon())),
}
]
on(responsive_button_over)
on(responsive_button_out)
}
}
@ -141,15 +151,63 @@ fn the_camera() -> impl Scene {
}
}
fn update_pitch_map_resource_ui(
query: Query<(Entity, &Note), With<Node>>,
pitch_map: Res<PitchMap>,
mut commands: Commands,
) {
query.iter().for_each(|(e, n)| {
if pitch_map.notes.contains(n) {
commands.entity(e).insert(Checked);
} else {
commands.entity(e).remove::<Checked>();
}
});
}
fn the_ui() -> impl Scene {
fn toggle_note(
this: On<Activate>,
notes: Query<&Note, With<UiButton>>,
mut pitch_map: ResMut<PitchMap>,
) {
let this_note = notes.get(this.entity).unwrap();
pitch_map.toggle(this_note);
}
bsn! {
Node {
top: Val::Px(0.0),
left: Val::Px(0.0),
height: Val::Px(40.0),
width: Val::Percent(100.0),
display: Display::Flex,
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Start,
}
Transform
pane()
Children [
pane_body()
Children [
subpane()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
subpane_header() Children [
Text("Nav") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_button(UiButton::Power)
on(|_: On<Activate>, mut writer: MessageWriter<AppExit>| {
@ -199,82 +257,91 @@ fn the_ui() -> impl Scene {
on(|_: On<Activate>, mut next: ResMut<NextState<AudioPlayback>>| {
next.set(AudioPlayback::Play);
}),
ui_button(UiButton::Note)
]
],
subpane()
Children [
subpane_header() Children [
Text("Notes") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
ui_checkbox("A")
Note::A
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("A#")
Note::ASharp
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("B")
Note::B
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("C")
Note::C
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("C#")
Note::CSharp
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("D")
Note::D
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("D#")
Note::DSharp
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("E")
Note::E
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("F")
Note::F
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("F#")
Note::FSharp
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("G")
Note::G
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::Note)
on(toggle_note),
ui_checkbox("G#")
Note::GSharp
on(|_: On<Activate>, pitch_map: ResMut<PitchMap>| {
warn!("TODO")
}),
ui_button(UiButton::AddLowOctave)
on(|_: On<Activate>| {
warn!("TODO")
}),
ui_button(UiButton::SubLowOctave)
on(|_: On<Activate>| {
warn!("TODO")
}),
ui_button(UiButton::AddHighOctave)
on(|_: On<Activate>| {
warn!("TODO")
}),
ui_button(UiButton::SubHighOctave)
on(|_: On<Activate>| {
warn!("TODO")
}),
on(toggle_note),
]
],
subpane()
subpane_header() Children [
Text("Octaves") ThemedText
]
subpane_body()
Node {
display: Display::Flex,
flex_direction: FlexDirection::Row,
align_items: AlignItems::Stretch,
justify_content: JustifyContent::Start,
}
Children [
@FeathersSlider { @max: 5., @value: 3., @min: 1. }
SliderStep(1.)
SliderPrecision(0)
on(|value_change: On<ValueChange<f32>>, mut pitch_map: ResMut<PitchMap>| {
if value_change.is_final {
pitch_map.octaves_up = value_change.value as usize;
}
})
on(slider_self_update),
@FeathersSlider { @max: 5., @value: 3., @min: 1. }
SliderStep(1.)
SliderPrecision(0)
on(|value_change: On<ValueChange<f32>>, mut pitch_map: ResMut<PitchMap>| {
if value_change.is_final {
pitch_map.octaves_down = value_change.value as usize;
}
})
on(slider_self_update),
],
]
]
}
}
@ -519,16 +586,6 @@ fn simulation_step(
});
}
fn responsive_button_over(trigger: On<Pointer<Over>>, mut bg: Query<&mut BackgroundColor>) {
let mut background_color = bg.get_mut(trigger.entity).unwrap();
background_color.0 = WHITE.into();
}
fn responsive_button_out(trigger: On<Pointer<Out>>, mut bg: Query<&mut BackgroundColor>) {
let mut background_color = bg.get_mut(trigger.entity).unwrap();
background_color.0 = GRAY.into();
}
/// Bring a cell to life, or kill it
#[derive(Message)]
enum Lifecycle {
@ -639,17 +696,34 @@ impl Note {
fn as_frequency(&self) -> f32 {
match self {
Self::A => 440.0,
Self::ASharp => todo!(),
Self::B => todo!(),
Self::C => todo!(),
Self::CSharp => todo!(),
Self::D => todo!(),
Self::DSharp => todo!(),
Self::E => todo!(),
Self::F => todo!(),
Self::FSharp => todo!(),
Self::G => todo!(),
Self::GSharp => todo!(),
Self::ASharp => 466.1638,
Self::B => 493.8833,
Self::C => 523.2511,
Self::CSharp => 554.3653,
Self::D => 587.3295,
Self::DSharp => 622.254,
Self::E => 659.2551,
Self::F => 698.4565,
Self::FSharp => 739.9888,
Self::G => 783.9909,
Self::GSharp => 830.6094,
}
}
fn _as_str(&self) -> &str {
match self {
Self::A => "A",
Self::ASharp => "A#",
Self::B => "B",
Self::C => "C",
Self::CSharp => "C#",
Self::D => "D",
Self::DSharp => "D#",
Self::E => "E",
Self::F => "F",
Self::FSharp => "F#",
Self::G => "G",
Self::GSharp => "G#",
}
}
}
@ -806,25 +880,16 @@ fn is_panning(panning: Res<Panning>) -> bool {
// This may or may not be an eye sore
fn create_light_theme() -> ThemeProps {
ThemeProps {
color: create_dark_theme()
.color
.iter()
.map(|(token, color)| {
let c = match color {
Color::Oklcha(oklcha) => Color::Oklcha(Oklcha {
lightness: oklcha.lightness + 0.1,
..*oklcha
}),
x => {
info!("Passing along color theme {:?} {:?}", token, x);
*x
}
};
(token.clone(), c)
})
.collect(),
}
build(
Seeds::from_coolors([
Srgba::hex("264653").unwrap().into(),
Srgba::hex("2A9D8F").unwrap().into(),
Srgba::hex("E9C46A").unwrap().into(),
Srgba::hex("F4A261").unwrap().into(),
Srgba::hex("E76F51").unwrap().into(),
]),
Mode::Light,
)
}
fn toggle_interactive<T: Component + States + PartialEq>(
@ -857,3 +922,13 @@ impl Default for PitchMap {
}
}
}
impl PitchMap {
fn toggle(&mut self, n: &Note) {
if let Some(idx) = self.notes.iter().position(|v| v == n) {
self.notes.swap_remove(idx);
} else {
self.notes.push(n.clone());
}
}
}

@ -8,7 +8,7 @@ if [[ "${TARGET}" == prototypes/* ]]; then
FLAGS="--bin $(basename "${TARGET}")"
else
cd "${TARGET}"
FLAGS=""
FLAGS="--features engine/dev"
fi
cargo run ${FLAGS}

Loading…
Cancel
Save