This commit is contained in:
@@ -7,6 +7,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
### Added
|
### Added
|
||||||
- Double-stack words: `2dup`, `2drop`, `2swap`, `2over`.
|
- Double-stack words: `2dup`, `2drop`, `2swap`, `2over`.
|
||||||
- `forget` word to remove user-defined words from the dictionary.
|
- `forget` word to remove user-defined words from the dictionary.
|
||||||
|
- Active patterns panel showing playing patterns with bank, pattern, iteration count, and step position.
|
||||||
|
- Configurable visualization layout (Top/Bottom/Left/Right) for scope and spectrum placement.
|
||||||
|
- Euclidean distribution modal to spread a step's script across the pattern using Euclidean rhythms.
|
||||||
|
- Fairyfloss theme (pastel candy colors by sailorhg).
|
||||||
|
- Hot Dog Stand theme (classic Windows 3.1 red/yellow).
|
||||||
|
- Hue rotation option in Options menu to shift all theme colors (0-360°).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Title view now adapts to smaller terminal sizes gracefully.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Scope/spectrum ratio asymmetry in Left/Right layout modes.
|
- Scope/spectrum ratio asymmetry in Left/Right layout modes.
|
||||||
|
|||||||
277
crates/ratatui/src/theme/fairyfloss.rs
Normal file
277
crates/ratatui/src/theme/fairyfloss.rs
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
use super::*;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
pub fn theme() -> ThemeColors {
|
||||||
|
let bg = Color::Rgb(90, 84, 117);
|
||||||
|
let bg_light = Color::Rgb(113, 103, 153);
|
||||||
|
let bg_lighter = Color::Rgb(130, 120, 165);
|
||||||
|
let fg = Color::Rgb(248, 248, 240);
|
||||||
|
let fg_dim = Color::Rgb(197, 163, 255);
|
||||||
|
let muted = Color::Rgb(168, 164, 177);
|
||||||
|
let dark = Color::Rgb(55, 51, 72);
|
||||||
|
|
||||||
|
let purple = Color::Rgb(174, 129, 255);
|
||||||
|
let pink = Color::Rgb(255, 184, 209);
|
||||||
|
let coral = Color::Rgb(255, 133, 127);
|
||||||
|
let yellow = Color::Rgb(255, 243, 82);
|
||||||
|
let gold = Color::Rgb(230, 192, 0);
|
||||||
|
let mint = Color::Rgb(194, 255, 223);
|
||||||
|
let lavender = Color::Rgb(197, 163, 255);
|
||||||
|
|
||||||
|
ThemeColors {
|
||||||
|
ui: UiColors {
|
||||||
|
bg,
|
||||||
|
bg_rgb: (90, 84, 117),
|
||||||
|
text_primary: fg,
|
||||||
|
text_muted: fg_dim,
|
||||||
|
text_dim: muted,
|
||||||
|
border: bg_lighter,
|
||||||
|
header: mint,
|
||||||
|
unfocused: muted,
|
||||||
|
accent: pink,
|
||||||
|
surface: bg_light,
|
||||||
|
},
|
||||||
|
status: StatusColors {
|
||||||
|
playing_bg: Color::Rgb(70, 95, 85),
|
||||||
|
playing_fg: mint,
|
||||||
|
stopped_bg: Color::Rgb(100, 70, 85),
|
||||||
|
stopped_fg: coral,
|
||||||
|
fill_on: mint,
|
||||||
|
fill_off: muted,
|
||||||
|
fill_bg: bg_light,
|
||||||
|
},
|
||||||
|
selection: SelectionColors {
|
||||||
|
cursor_bg: pink,
|
||||||
|
cursor_fg: dark,
|
||||||
|
selected_bg: Color::Rgb(120, 90, 130),
|
||||||
|
selected_fg: pink,
|
||||||
|
in_range_bg: Color::Rgb(100, 95, 125),
|
||||||
|
in_range_fg: fg,
|
||||||
|
cursor: pink,
|
||||||
|
selected: Color::Rgb(120, 90, 130),
|
||||||
|
in_range: Color::Rgb(100, 95, 125),
|
||||||
|
},
|
||||||
|
tile: TileColors {
|
||||||
|
playing_active_bg: Color::Rgb(100, 85, 60),
|
||||||
|
playing_active_fg: gold,
|
||||||
|
playing_inactive_bg: Color::Rgb(95, 90, 70),
|
||||||
|
playing_inactive_fg: yellow,
|
||||||
|
active_bg: Color::Rgb(70, 100, 100),
|
||||||
|
active_fg: mint,
|
||||||
|
inactive_bg: bg_light,
|
||||||
|
inactive_fg: fg_dim,
|
||||||
|
active_selected_bg: Color::Rgb(120, 90, 130),
|
||||||
|
active_in_range_bg: Color::Rgb(100, 95, 125),
|
||||||
|
link_bright: [
|
||||||
|
(255, 184, 209),
|
||||||
|
(174, 129, 255),
|
||||||
|
(255, 133, 127),
|
||||||
|
(194, 255, 223),
|
||||||
|
(255, 243, 82),
|
||||||
|
],
|
||||||
|
link_dim: [
|
||||||
|
(100, 75, 90),
|
||||||
|
(85, 70, 105),
|
||||||
|
(100, 65, 65),
|
||||||
|
(75, 100, 95),
|
||||||
|
(100, 95, 55),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
header: HeaderColors {
|
||||||
|
tempo_bg: Color::Rgb(100, 75, 95),
|
||||||
|
tempo_fg: pink,
|
||||||
|
bank_bg: Color::Rgb(70, 95, 95),
|
||||||
|
bank_fg: mint,
|
||||||
|
pattern_bg: Color::Rgb(85, 75, 110),
|
||||||
|
pattern_fg: purple,
|
||||||
|
stats_bg: bg_light,
|
||||||
|
stats_fg: fg_dim,
|
||||||
|
},
|
||||||
|
modal: ModalColors {
|
||||||
|
border: mint,
|
||||||
|
border_accent: pink,
|
||||||
|
border_warn: coral,
|
||||||
|
border_dim: muted,
|
||||||
|
confirm: coral,
|
||||||
|
rename: purple,
|
||||||
|
input: mint,
|
||||||
|
editor: mint,
|
||||||
|
preview: muted,
|
||||||
|
},
|
||||||
|
flash: FlashColors {
|
||||||
|
error_bg: Color::Rgb(100, 65, 70),
|
||||||
|
error_fg: coral,
|
||||||
|
success_bg: Color::Rgb(65, 95, 85),
|
||||||
|
success_fg: mint,
|
||||||
|
info_bg: bg_light,
|
||||||
|
info_fg: fg,
|
||||||
|
event_rgb: (100, 85, 110),
|
||||||
|
},
|
||||||
|
list: ListColors {
|
||||||
|
playing_bg: Color::Rgb(65, 95, 85),
|
||||||
|
playing_fg: mint,
|
||||||
|
staged_play_bg: Color::Rgb(95, 80, 120),
|
||||||
|
staged_play_fg: purple,
|
||||||
|
staged_stop_bg: Color::Rgb(105, 70, 85),
|
||||||
|
staged_stop_fg: pink,
|
||||||
|
edit_bg: Color::Rgb(70, 95, 100),
|
||||||
|
edit_fg: mint,
|
||||||
|
hover_bg: bg_lighter,
|
||||||
|
hover_fg: fg,
|
||||||
|
},
|
||||||
|
link_status: LinkStatusColors {
|
||||||
|
disabled: coral,
|
||||||
|
connected: mint,
|
||||||
|
listening: yellow,
|
||||||
|
},
|
||||||
|
syntax: SyntaxColors {
|
||||||
|
gap_bg: dark,
|
||||||
|
executed_bg: Color::Rgb(80, 75, 100),
|
||||||
|
selected_bg: Color::Rgb(110, 100, 70),
|
||||||
|
emit: (fg, Color::Rgb(110, 80, 100)),
|
||||||
|
number: (purple, Color::Rgb(85, 75, 110)),
|
||||||
|
string: (yellow, Color::Rgb(100, 95, 60)),
|
||||||
|
comment: (muted, dark),
|
||||||
|
keyword: (pink, Color::Rgb(105, 75, 90)),
|
||||||
|
stack_op: (mint, Color::Rgb(70, 100, 95)),
|
||||||
|
operator: (pink, Color::Rgb(105, 75, 90)),
|
||||||
|
sound: (mint, Color::Rgb(70, 100, 95)),
|
||||||
|
param: (coral, Color::Rgb(105, 70, 70)),
|
||||||
|
context: (coral, Color::Rgb(105, 70, 70)),
|
||||||
|
note: (lavender, Color::Rgb(85, 75, 110)),
|
||||||
|
interval: (Color::Rgb(220, 190, 255), Color::Rgb(85, 75, 100)),
|
||||||
|
variable: (lavender, Color::Rgb(85, 75, 110)),
|
||||||
|
vary: (yellow, Color::Rgb(100, 95, 60)),
|
||||||
|
generator: (mint, Color::Rgb(70, 95, 95)),
|
||||||
|
default: (fg_dim, dark),
|
||||||
|
},
|
||||||
|
table: TableColors {
|
||||||
|
row_even: dark,
|
||||||
|
row_odd: bg,
|
||||||
|
},
|
||||||
|
values: ValuesColors {
|
||||||
|
tempo: coral,
|
||||||
|
value: fg_dim,
|
||||||
|
},
|
||||||
|
hint: HintColors {
|
||||||
|
key: coral,
|
||||||
|
text: muted,
|
||||||
|
},
|
||||||
|
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||||
|
nav: NavColors {
|
||||||
|
selected_bg: Color::Rgb(110, 85, 120),
|
||||||
|
selected_fg: fg,
|
||||||
|
unselected_bg: bg_light,
|
||||||
|
unselected_fg: muted,
|
||||||
|
},
|
||||||
|
editor_widget: EditorWidgetColors {
|
||||||
|
cursor_bg: fg,
|
||||||
|
cursor_fg: bg,
|
||||||
|
selection_bg: Color::Rgb(105, 95, 125),
|
||||||
|
completion_bg: bg_light,
|
||||||
|
completion_fg: fg,
|
||||||
|
completion_selected: coral,
|
||||||
|
completion_example: mint,
|
||||||
|
},
|
||||||
|
browser: BrowserColors {
|
||||||
|
directory: mint,
|
||||||
|
project_file: purple,
|
||||||
|
selected: coral,
|
||||||
|
file: fg,
|
||||||
|
focused_border: coral,
|
||||||
|
unfocused_border: muted,
|
||||||
|
root: fg,
|
||||||
|
file_icon: muted,
|
||||||
|
folder_icon: mint,
|
||||||
|
empty_text: muted,
|
||||||
|
},
|
||||||
|
input: InputColors {
|
||||||
|
text: mint,
|
||||||
|
cursor: fg,
|
||||||
|
hint: muted,
|
||||||
|
},
|
||||||
|
search: SearchColors {
|
||||||
|
active: coral,
|
||||||
|
inactive: muted,
|
||||||
|
match_bg: yellow,
|
||||||
|
match_fg: dark,
|
||||||
|
},
|
||||||
|
markdown: MarkdownColors {
|
||||||
|
h1: mint,
|
||||||
|
h2: coral,
|
||||||
|
h3: purple,
|
||||||
|
code: lavender,
|
||||||
|
code_border: Color::Rgb(120, 115, 140),
|
||||||
|
link: pink,
|
||||||
|
link_url: Color::Rgb(150, 145, 165),
|
||||||
|
quote: muted,
|
||||||
|
text: fg,
|
||||||
|
list: fg,
|
||||||
|
},
|
||||||
|
engine: EngineColors {
|
||||||
|
header: mint,
|
||||||
|
header_focused: yellow,
|
||||||
|
divider: Color::Rgb(110, 105, 130),
|
||||||
|
scroll_indicator: Color::Rgb(125, 120, 145),
|
||||||
|
label: Color::Rgb(175, 170, 190),
|
||||||
|
label_focused: Color::Rgb(210, 205, 225),
|
||||||
|
label_dim: Color::Rgb(145, 140, 160),
|
||||||
|
value: Color::Rgb(230, 225, 240),
|
||||||
|
focused: yellow,
|
||||||
|
normal: fg,
|
||||||
|
dim: Color::Rgb(125, 120, 145),
|
||||||
|
path: Color::Rgb(175, 170, 190),
|
||||||
|
border_magenta: pink,
|
||||||
|
border_green: mint,
|
||||||
|
border_cyan: lavender,
|
||||||
|
separator: Color::Rgb(110, 105, 130),
|
||||||
|
hint_active: Color::Rgb(240, 230, 120),
|
||||||
|
hint_inactive: Color::Rgb(110, 105, 130),
|
||||||
|
},
|
||||||
|
dict: DictColors {
|
||||||
|
word_name: lavender,
|
||||||
|
word_bg: Color::Rgb(75, 85, 105),
|
||||||
|
alias: muted,
|
||||||
|
stack_sig: purple,
|
||||||
|
description: fg,
|
||||||
|
example: Color::Rgb(175, 170, 190),
|
||||||
|
category_focused: yellow,
|
||||||
|
category_selected: mint,
|
||||||
|
category_normal: fg,
|
||||||
|
category_dimmed: Color::Rgb(125, 120, 145),
|
||||||
|
border_focused: yellow,
|
||||||
|
border_normal: Color::Rgb(110, 105, 130),
|
||||||
|
header_desc: Color::Rgb(195, 190, 210),
|
||||||
|
},
|
||||||
|
title: TitleColors {
|
||||||
|
big_title: pink,
|
||||||
|
author: mint,
|
||||||
|
link: lavender,
|
||||||
|
license: coral,
|
||||||
|
prompt: Color::Rgb(195, 190, 210),
|
||||||
|
subtitle: fg,
|
||||||
|
},
|
||||||
|
meter: MeterColors {
|
||||||
|
low: mint,
|
||||||
|
mid: yellow,
|
||||||
|
high: coral,
|
||||||
|
low_rgb: (194, 255, 223),
|
||||||
|
mid_rgb: (255, 243, 82),
|
||||||
|
high_rgb: (255, 133, 127),
|
||||||
|
},
|
||||||
|
sparkle: SparkleColors {
|
||||||
|
colors: [
|
||||||
|
(194, 255, 223),
|
||||||
|
(255, 133, 127),
|
||||||
|
(255, 243, 82),
|
||||||
|
(255, 184, 209),
|
||||||
|
(174, 129, 255),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
confirm: ConfirmColors {
|
||||||
|
border: coral,
|
||||||
|
button_selected_bg: coral,
|
||||||
|
button_selected_fg: dark,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
273
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
273
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
use super::*;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
pub fn theme() -> ThemeColors {
|
||||||
|
let red = Color::Rgb(255, 0, 0);
|
||||||
|
let dark_red = Color::Rgb(215, 0, 0);
|
||||||
|
let darker_red = Color::Rgb(175, 0, 0);
|
||||||
|
let yellow = Color::Rgb(255, 255, 0);
|
||||||
|
let light_yellow = Color::Rgb(255, 255, 95);
|
||||||
|
let gold = Color::Rgb(255, 215, 0);
|
||||||
|
let black = Color::Rgb(0, 0, 0);
|
||||||
|
let white = Color::Rgb(255, 255, 255);
|
||||||
|
|
||||||
|
let dim_yellow = Color::Rgb(180, 180, 0);
|
||||||
|
let muted_red = Color::Rgb(140, 40, 40);
|
||||||
|
|
||||||
|
ThemeColors {
|
||||||
|
ui: UiColors {
|
||||||
|
bg: red,
|
||||||
|
bg_rgb: (255, 0, 0),
|
||||||
|
text_primary: yellow,
|
||||||
|
text_muted: light_yellow,
|
||||||
|
text_dim: gold,
|
||||||
|
border: yellow,
|
||||||
|
header: yellow,
|
||||||
|
unfocused: gold,
|
||||||
|
accent: yellow,
|
||||||
|
surface: dark_red,
|
||||||
|
},
|
||||||
|
status: StatusColors {
|
||||||
|
playing_bg: Color::Rgb(180, 180, 0),
|
||||||
|
playing_fg: black,
|
||||||
|
stopped_bg: darker_red,
|
||||||
|
stopped_fg: yellow,
|
||||||
|
fill_on: yellow,
|
||||||
|
fill_off: gold,
|
||||||
|
fill_bg: dark_red,
|
||||||
|
},
|
||||||
|
selection: SelectionColors {
|
||||||
|
cursor_bg: yellow,
|
||||||
|
cursor_fg: red,
|
||||||
|
selected_bg: Color::Rgb(200, 200, 0),
|
||||||
|
selected_fg: black,
|
||||||
|
in_range_bg: Color::Rgb(170, 100, 0),
|
||||||
|
in_range_fg: yellow,
|
||||||
|
cursor: yellow,
|
||||||
|
selected: Color::Rgb(200, 200, 0),
|
||||||
|
in_range: Color::Rgb(170, 100, 0),
|
||||||
|
},
|
||||||
|
tile: TileColors {
|
||||||
|
playing_active_bg: Color::Rgb(200, 200, 0),
|
||||||
|
playing_active_fg: black,
|
||||||
|
playing_inactive_bg: Color::Rgb(180, 180, 0),
|
||||||
|
playing_inactive_fg: black,
|
||||||
|
active_bg: Color::Rgb(200, 50, 50),
|
||||||
|
active_fg: yellow,
|
||||||
|
inactive_bg: dark_red,
|
||||||
|
inactive_fg: gold,
|
||||||
|
active_selected_bg: Color::Rgb(200, 200, 0),
|
||||||
|
active_in_range_bg: Color::Rgb(170, 100, 0),
|
||||||
|
link_bright: [
|
||||||
|
(255, 255, 0),
|
||||||
|
(255, 255, 255),
|
||||||
|
(255, 215, 0),
|
||||||
|
(255, 255, 95),
|
||||||
|
(255, 255, 0),
|
||||||
|
],
|
||||||
|
link_dim: [
|
||||||
|
(140, 140, 0),
|
||||||
|
(140, 140, 140),
|
||||||
|
(140, 120, 0),
|
||||||
|
(140, 140, 60),
|
||||||
|
(140, 140, 0),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
header: HeaderColors {
|
||||||
|
tempo_bg: Color::Rgb(180, 180, 0),
|
||||||
|
tempo_fg: black,
|
||||||
|
bank_bg: darker_red,
|
||||||
|
bank_fg: yellow,
|
||||||
|
pattern_bg: Color::Rgb(200, 200, 0),
|
||||||
|
pattern_fg: black,
|
||||||
|
stats_bg: dark_red,
|
||||||
|
stats_fg: yellow,
|
||||||
|
},
|
||||||
|
modal: ModalColors {
|
||||||
|
border: yellow,
|
||||||
|
border_accent: white,
|
||||||
|
border_warn: gold,
|
||||||
|
border_dim: dim_yellow,
|
||||||
|
confirm: gold,
|
||||||
|
rename: light_yellow,
|
||||||
|
input: yellow,
|
||||||
|
editor: yellow,
|
||||||
|
preview: gold,
|
||||||
|
},
|
||||||
|
flash: FlashColors {
|
||||||
|
error_bg: black,
|
||||||
|
error_fg: yellow,
|
||||||
|
success_bg: Color::Rgb(180, 180, 0),
|
||||||
|
success_fg: black,
|
||||||
|
info_bg: dark_red,
|
||||||
|
info_fg: yellow,
|
||||||
|
event_rgb: (170, 100, 0),
|
||||||
|
},
|
||||||
|
list: ListColors {
|
||||||
|
playing_bg: Color::Rgb(180, 180, 0),
|
||||||
|
playing_fg: black,
|
||||||
|
staged_play_bg: Color::Rgb(200, 200, 0),
|
||||||
|
staged_play_fg: black,
|
||||||
|
staged_stop_bg: darker_red,
|
||||||
|
staged_stop_fg: yellow,
|
||||||
|
edit_bg: Color::Rgb(200, 50, 50),
|
||||||
|
edit_fg: yellow,
|
||||||
|
hover_bg: Color::Rgb(230, 50, 50),
|
||||||
|
hover_fg: yellow,
|
||||||
|
},
|
||||||
|
link_status: LinkStatusColors {
|
||||||
|
disabled: white,
|
||||||
|
connected: yellow,
|
||||||
|
listening: gold,
|
||||||
|
},
|
||||||
|
syntax: SyntaxColors {
|
||||||
|
gap_bg: darker_red,
|
||||||
|
executed_bg: Color::Rgb(200, 50, 50),
|
||||||
|
selected_bg: Color::Rgb(180, 180, 0),
|
||||||
|
emit: (yellow, muted_red),
|
||||||
|
number: (white, muted_red),
|
||||||
|
string: (gold, muted_red),
|
||||||
|
comment: (dim_yellow, darker_red),
|
||||||
|
keyword: (light_yellow, muted_red),
|
||||||
|
stack_op: (yellow, muted_red),
|
||||||
|
operator: (light_yellow, muted_red),
|
||||||
|
sound: (yellow, muted_red),
|
||||||
|
param: (gold, muted_red),
|
||||||
|
context: (gold, muted_red),
|
||||||
|
note: (white, muted_red),
|
||||||
|
interval: (Color::Rgb(255, 240, 150), muted_red),
|
||||||
|
variable: (white, muted_red),
|
||||||
|
vary: (gold, muted_red),
|
||||||
|
generator: (yellow, muted_red),
|
||||||
|
default: (light_yellow, darker_red),
|
||||||
|
},
|
||||||
|
table: TableColors {
|
||||||
|
row_even: darker_red,
|
||||||
|
row_odd: red,
|
||||||
|
},
|
||||||
|
values: ValuesColors {
|
||||||
|
tempo: gold,
|
||||||
|
value: light_yellow,
|
||||||
|
},
|
||||||
|
hint: HintColors {
|
||||||
|
key: white,
|
||||||
|
text: gold,
|
||||||
|
},
|
||||||
|
view_badge: ViewBadgeColors { bg: yellow, fg: red },
|
||||||
|
nav: NavColors {
|
||||||
|
selected_bg: Color::Rgb(200, 200, 0),
|
||||||
|
selected_fg: black,
|
||||||
|
unselected_bg: dark_red,
|
||||||
|
unselected_fg: gold,
|
||||||
|
},
|
||||||
|
editor_widget: EditorWidgetColors {
|
||||||
|
cursor_bg: yellow,
|
||||||
|
cursor_fg: red,
|
||||||
|
selection_bg: Color::Rgb(180, 180, 0),
|
||||||
|
completion_bg: dark_red,
|
||||||
|
completion_fg: yellow,
|
||||||
|
completion_selected: white,
|
||||||
|
completion_example: gold,
|
||||||
|
},
|
||||||
|
browser: BrowserColors {
|
||||||
|
directory: yellow,
|
||||||
|
project_file: white,
|
||||||
|
selected: gold,
|
||||||
|
file: light_yellow,
|
||||||
|
focused_border: white,
|
||||||
|
unfocused_border: gold,
|
||||||
|
root: yellow,
|
||||||
|
file_icon: gold,
|
||||||
|
folder_icon: yellow,
|
||||||
|
empty_text: gold,
|
||||||
|
},
|
||||||
|
input: InputColors {
|
||||||
|
text: yellow,
|
||||||
|
cursor: white,
|
||||||
|
hint: gold,
|
||||||
|
},
|
||||||
|
search: SearchColors {
|
||||||
|
active: white,
|
||||||
|
inactive: gold,
|
||||||
|
match_bg: yellow,
|
||||||
|
match_fg: red,
|
||||||
|
},
|
||||||
|
markdown: MarkdownColors {
|
||||||
|
h1: yellow,
|
||||||
|
h2: white,
|
||||||
|
h3: gold,
|
||||||
|
code: light_yellow,
|
||||||
|
code_border: dim_yellow,
|
||||||
|
link: white,
|
||||||
|
link_url: gold,
|
||||||
|
quote: dim_yellow,
|
||||||
|
text: yellow,
|
||||||
|
list: yellow,
|
||||||
|
},
|
||||||
|
engine: EngineColors {
|
||||||
|
header: yellow,
|
||||||
|
header_focused: white,
|
||||||
|
divider: dim_yellow,
|
||||||
|
scroll_indicator: gold,
|
||||||
|
label: light_yellow,
|
||||||
|
label_focused: white,
|
||||||
|
label_dim: dim_yellow,
|
||||||
|
value: yellow,
|
||||||
|
focused: white,
|
||||||
|
normal: yellow,
|
||||||
|
dim: dim_yellow,
|
||||||
|
path: gold,
|
||||||
|
border_magenta: gold,
|
||||||
|
border_green: yellow,
|
||||||
|
border_cyan: white,
|
||||||
|
separator: dim_yellow,
|
||||||
|
hint_active: white,
|
||||||
|
hint_inactive: dim_yellow,
|
||||||
|
},
|
||||||
|
dict: DictColors {
|
||||||
|
word_name: yellow,
|
||||||
|
word_bg: darker_red,
|
||||||
|
alias: gold,
|
||||||
|
stack_sig: white,
|
||||||
|
description: yellow,
|
||||||
|
example: gold,
|
||||||
|
category_focused: white,
|
||||||
|
category_selected: yellow,
|
||||||
|
category_normal: light_yellow,
|
||||||
|
category_dimmed: dim_yellow,
|
||||||
|
border_focused: white,
|
||||||
|
border_normal: dim_yellow,
|
||||||
|
header_desc: gold,
|
||||||
|
},
|
||||||
|
title: TitleColors {
|
||||||
|
big_title: yellow,
|
||||||
|
author: white,
|
||||||
|
link: gold,
|
||||||
|
license: light_yellow,
|
||||||
|
prompt: gold,
|
||||||
|
subtitle: yellow,
|
||||||
|
},
|
||||||
|
meter: MeterColors {
|
||||||
|
low: yellow,
|
||||||
|
mid: gold,
|
||||||
|
high: white,
|
||||||
|
low_rgb: (255, 255, 0),
|
||||||
|
mid_rgb: (255, 215, 0),
|
||||||
|
high_rgb: (255, 255, 255),
|
||||||
|
},
|
||||||
|
sparkle: SparkleColors {
|
||||||
|
colors: [
|
||||||
|
(255, 255, 0),
|
||||||
|
(255, 255, 255),
|
||||||
|
(255, 215, 0),
|
||||||
|
(255, 255, 95),
|
||||||
|
(255, 255, 0),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
confirm: ConfirmColors {
|
||||||
|
border: white,
|
||||||
|
button_selected_bg: yellow,
|
||||||
|
button_selected_fg: red,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ pub fn theme() -> ThemeColors {
|
|||||||
let autumn_red = Color::Rgb(195, 64, 67);
|
let autumn_red = Color::Rgb(195, 64, 67);
|
||||||
let carp_yellow = Color::Rgb(230, 195, 132);
|
let carp_yellow = Color::Rgb(230, 195, 132);
|
||||||
let spring_blue = Color::Rgb(127, 180, 202);
|
let spring_blue = Color::Rgb(127, 180, 202);
|
||||||
let wave_red = Color::Rgb(226, 109, 115);
|
let wave_red = Color::Rgb(228, 104, 118);
|
||||||
let sakura_pink = Color::Rgb(212, 140, 149);
|
let sakura_pink = Color::Rgb(210, 126, 153);
|
||||||
|
|
||||||
let darker_bg = Color::Rgb(26, 26, 34);
|
let darker_bg = Color::Rgb(26, 26, 34);
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
active_selected_bg: Color::Rgb(65, 55, 70),
|
active_selected_bg: Color::Rgb(65, 55, 70),
|
||||||
active_in_range_bg: Color::Rgb(50, 50, 60),
|
active_in_range_bg: Color::Rgb(50, 50, 60),
|
||||||
link_bright: [
|
link_bright: [
|
||||||
(226, 109, 115),
|
(228, 104, 118),
|
||||||
(149, 127, 184),
|
(149, 127, 184),
|
||||||
(230, 195, 132),
|
(230, 195, 132),
|
||||||
(127, 180, 202),
|
(127, 180, 202),
|
||||||
@@ -258,14 +258,14 @@ pub fn theme() -> ThemeColors {
|
|||||||
high: wave_red,
|
high: wave_red,
|
||||||
low_rgb: (118, 148, 106),
|
low_rgb: (118, 148, 106),
|
||||||
mid_rgb: (230, 195, 132),
|
mid_rgb: (230, 195, 132),
|
||||||
high_rgb: (226, 109, 115),
|
high_rgb: (228, 104, 118),
|
||||||
},
|
},
|
||||||
sparkle: SparkleColors {
|
sparkle: SparkleColors {
|
||||||
colors: [
|
colors: [
|
||||||
(127, 180, 202),
|
(127, 180, 202),
|
||||||
(230, 195, 132),
|
(230, 195, 132),
|
||||||
(118, 148, 106),
|
(118, 148, 106),
|
||||||
(226, 109, 115),
|
(228, 104, 118),
|
||||||
(149, 127, 184),
|
(149, 127, 184),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
mod catppuccin_latte;
|
mod catppuccin_latte;
|
||||||
mod catppuccin_mocha;
|
mod catppuccin_mocha;
|
||||||
mod dracula;
|
mod dracula;
|
||||||
|
mod fairyfloss;
|
||||||
mod gruvbox_dark;
|
mod gruvbox_dark;
|
||||||
|
mod hot_dog_stand;
|
||||||
mod kanagawa;
|
mod kanagawa;
|
||||||
mod monochrome_black;
|
mod monochrome_black;
|
||||||
mod monochrome_white;
|
mod monochrome_white;
|
||||||
@@ -13,6 +15,7 @@ mod nord;
|
|||||||
mod pitch_black;
|
mod pitch_black;
|
||||||
mod rose_pine;
|
mod rose_pine;
|
||||||
mod tokyo_night;
|
mod tokyo_night;
|
||||||
|
pub mod transform;
|
||||||
|
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -36,6 +39,8 @@ pub const THEMES: &[ThemeEntry] = &[
|
|||||||
ThemeEntry { id: "TokyoNight", label: "Tokyo Night", colors: tokyo_night::theme },
|
ThemeEntry { id: "TokyoNight", label: "Tokyo Night", colors: tokyo_night::theme },
|
||||||
ThemeEntry { id: "RosePine", label: "Rosé Pine", colors: rose_pine::theme },
|
ThemeEntry { id: "RosePine", label: "Rosé Pine", colors: rose_pine::theme },
|
||||||
ThemeEntry { id: "Kanagawa", label: "Kanagawa", colors: kanagawa::theme },
|
ThemeEntry { id: "Kanagawa", label: "Kanagawa", colors: kanagawa::theme },
|
||||||
|
ThemeEntry { id: "Fairyfloss", label: "Fairyfloss", colors: fairyfloss::theme },
|
||||||
|
ThemeEntry { id: "HotDogStand", label: "Hot Dog Stand", colors: hot_dog_stand::theme },
|
||||||
];
|
];
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub fn theme() -> ThemeColors {
|
|||||||
let fg = Color::Rgb(224, 222, 244);
|
let fg = Color::Rgb(224, 222, 244);
|
||||||
let fg_dim = Color::Rgb(144, 140, 170);
|
let fg_dim = Color::Rgb(144, 140, 170);
|
||||||
let muted = Color::Rgb(110, 106, 134);
|
let muted = Color::Rgb(110, 106, 134);
|
||||||
let rose = Color::Rgb(235, 111, 146);
|
let rose = Color::Rgb(235, 188, 186);
|
||||||
let gold = Color::Rgb(246, 193, 119);
|
let gold = Color::Rgb(246, 193, 119);
|
||||||
let foam = Color::Rgb(156, 207, 216);
|
let foam = Color::Rgb(156, 207, 216);
|
||||||
let iris = Color::Rgb(196, 167, 231);
|
let iris = Color::Rgb(196, 167, 231);
|
||||||
|
|||||||
345
crates/ratatui/src/theme/transform.rs
Normal file
345
crates/ratatui/src/theme/transform.rs
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
use ratatui::style::Color;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
|
||||||
|
let r = r as f32 / 255.0;
|
||||||
|
let g = g as f32 / 255.0;
|
||||||
|
let b = b as f32 / 255.0;
|
||||||
|
|
||||||
|
let max = r.max(g).max(b);
|
||||||
|
let min = r.min(g).min(b);
|
||||||
|
let delta = max - min;
|
||||||
|
|
||||||
|
let h = if delta == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else if max == r {
|
||||||
|
60.0 * (((g - b) / delta) % 6.0)
|
||||||
|
} else if max == g {
|
||||||
|
60.0 * (((b - r) / delta) + 2.0)
|
||||||
|
} else {
|
||||||
|
60.0 * (((r - g) / delta) + 4.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = if h < 0.0 { h + 360.0 } else { h };
|
||||||
|
let s = if max == 0.0 { 0.0 } else { delta / max };
|
||||||
|
let v = max;
|
||||||
|
|
||||||
|
(h, s, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
|
||||||
|
let c = v * s;
|
||||||
|
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||||
|
let m = v - c;
|
||||||
|
|
||||||
|
let (r, g, b) = if h < 60.0 {
|
||||||
|
(c, x, 0.0)
|
||||||
|
} else if h < 120.0 {
|
||||||
|
(x, c, 0.0)
|
||||||
|
} else if h < 180.0 {
|
||||||
|
(0.0, c, x)
|
||||||
|
} else if h < 240.0 {
|
||||||
|
(0.0, x, c)
|
||||||
|
} else if h < 300.0 {
|
||||||
|
(x, 0.0, c)
|
||||||
|
} else {
|
||||||
|
(c, 0.0, x)
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
((r + m) * 255.0) as u8,
|
||||||
|
((g + m) * 255.0) as u8,
|
||||||
|
((b + m) * 255.0) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_hue_rgb(r: u8, g: u8, b: u8, degrees: f32) -> (u8, u8, u8) {
|
||||||
|
let (h, s, v) = rgb_to_hsv(r, g, b);
|
||||||
|
let new_h = (h + degrees) % 360.0;
|
||||||
|
let new_h = if new_h < 0.0 { new_h + 360.0 } else { new_h };
|
||||||
|
hsv_to_rgb(new_h, s, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_color(color: Color, degrees: f32) -> Color {
|
||||||
|
match color {
|
||||||
|
Color::Rgb(r, g, b) => {
|
||||||
|
let (nr, ng, nb) = rotate_hue_rgb(r, g, b, degrees);
|
||||||
|
Color::Rgb(nr, ng, nb)
|
||||||
|
}
|
||||||
|
_ => color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_tuple(tuple: (u8, u8, u8), degrees: f32) -> (u8, u8, u8) {
|
||||||
|
rotate_hue_rgb(tuple.0, tuple.1, tuple.2, degrees)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rotate_color_pair(pair: (Color, Color), degrees: f32) -> (Color, Color) {
|
||||||
|
(rotate_color(pair.0, degrees), rotate_color(pair.1, degrees))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
||||||
|
if degrees == 0.0 {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeColors {
|
||||||
|
ui: UiColors {
|
||||||
|
bg: rotate_color(theme.ui.bg, degrees),
|
||||||
|
bg_rgb: rotate_tuple(theme.ui.bg_rgb, degrees),
|
||||||
|
text_primary: rotate_color(theme.ui.text_primary, degrees),
|
||||||
|
text_muted: rotate_color(theme.ui.text_muted, degrees),
|
||||||
|
text_dim: rotate_color(theme.ui.text_dim, degrees),
|
||||||
|
border: rotate_color(theme.ui.border, degrees),
|
||||||
|
header: rotate_color(theme.ui.header, degrees),
|
||||||
|
unfocused: rotate_color(theme.ui.unfocused, degrees),
|
||||||
|
accent: rotate_color(theme.ui.accent, degrees),
|
||||||
|
surface: rotate_color(theme.ui.surface, degrees),
|
||||||
|
},
|
||||||
|
status: StatusColors {
|
||||||
|
playing_bg: rotate_color(theme.status.playing_bg, degrees),
|
||||||
|
playing_fg: rotate_color(theme.status.playing_fg, degrees),
|
||||||
|
stopped_bg: rotate_color(theme.status.stopped_bg, degrees),
|
||||||
|
stopped_fg: rotate_color(theme.status.stopped_fg, degrees),
|
||||||
|
fill_on: rotate_color(theme.status.fill_on, degrees),
|
||||||
|
fill_off: rotate_color(theme.status.fill_off, degrees),
|
||||||
|
fill_bg: rotate_color(theme.status.fill_bg, degrees),
|
||||||
|
},
|
||||||
|
selection: SelectionColors {
|
||||||
|
cursor_bg: rotate_color(theme.selection.cursor_bg, degrees),
|
||||||
|
cursor_fg: rotate_color(theme.selection.cursor_fg, degrees),
|
||||||
|
selected_bg: rotate_color(theme.selection.selected_bg, degrees),
|
||||||
|
selected_fg: rotate_color(theme.selection.selected_fg, degrees),
|
||||||
|
in_range_bg: rotate_color(theme.selection.in_range_bg, degrees),
|
||||||
|
in_range_fg: rotate_color(theme.selection.in_range_fg, degrees),
|
||||||
|
cursor: rotate_color(theme.selection.cursor, degrees),
|
||||||
|
selected: rotate_color(theme.selection.selected, degrees),
|
||||||
|
in_range: rotate_color(theme.selection.in_range, degrees),
|
||||||
|
},
|
||||||
|
tile: TileColors {
|
||||||
|
playing_active_bg: rotate_color(theme.tile.playing_active_bg, degrees),
|
||||||
|
playing_active_fg: rotate_color(theme.tile.playing_active_fg, degrees),
|
||||||
|
playing_inactive_bg: rotate_color(theme.tile.playing_inactive_bg, degrees),
|
||||||
|
playing_inactive_fg: rotate_color(theme.tile.playing_inactive_fg, degrees),
|
||||||
|
active_bg: rotate_color(theme.tile.active_bg, degrees),
|
||||||
|
active_fg: rotate_color(theme.tile.active_fg, degrees),
|
||||||
|
inactive_bg: rotate_color(theme.tile.inactive_bg, degrees),
|
||||||
|
inactive_fg: rotate_color(theme.tile.inactive_fg, degrees),
|
||||||
|
active_selected_bg: rotate_color(theme.tile.active_selected_bg, degrees),
|
||||||
|
active_in_range_bg: rotate_color(theme.tile.active_in_range_bg, degrees),
|
||||||
|
link_bright: [
|
||||||
|
rotate_tuple(theme.tile.link_bright[0], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_bright[1], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_bright[2], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_bright[3], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_bright[4], degrees),
|
||||||
|
],
|
||||||
|
link_dim: [
|
||||||
|
rotate_tuple(theme.tile.link_dim[0], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_dim[1], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_dim[2], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_dim[3], degrees),
|
||||||
|
rotate_tuple(theme.tile.link_dim[4], degrees),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
header: HeaderColors {
|
||||||
|
tempo_bg: rotate_color(theme.header.tempo_bg, degrees),
|
||||||
|
tempo_fg: rotate_color(theme.header.tempo_fg, degrees),
|
||||||
|
bank_bg: rotate_color(theme.header.bank_bg, degrees),
|
||||||
|
bank_fg: rotate_color(theme.header.bank_fg, degrees),
|
||||||
|
pattern_bg: rotate_color(theme.header.pattern_bg, degrees),
|
||||||
|
pattern_fg: rotate_color(theme.header.pattern_fg, degrees),
|
||||||
|
stats_bg: rotate_color(theme.header.stats_bg, degrees),
|
||||||
|
stats_fg: rotate_color(theme.header.stats_fg, degrees),
|
||||||
|
},
|
||||||
|
modal: ModalColors {
|
||||||
|
border: rotate_color(theme.modal.border, degrees),
|
||||||
|
border_accent: rotate_color(theme.modal.border_accent, degrees),
|
||||||
|
border_warn: rotate_color(theme.modal.border_warn, degrees),
|
||||||
|
border_dim: rotate_color(theme.modal.border_dim, degrees),
|
||||||
|
confirm: rotate_color(theme.modal.confirm, degrees),
|
||||||
|
rename: rotate_color(theme.modal.rename, degrees),
|
||||||
|
input: rotate_color(theme.modal.input, degrees),
|
||||||
|
editor: rotate_color(theme.modal.editor, degrees),
|
||||||
|
preview: rotate_color(theme.modal.preview, degrees),
|
||||||
|
},
|
||||||
|
flash: FlashColors {
|
||||||
|
error_bg: rotate_color(theme.flash.error_bg, degrees),
|
||||||
|
error_fg: rotate_color(theme.flash.error_fg, degrees),
|
||||||
|
success_bg: rotate_color(theme.flash.success_bg, degrees),
|
||||||
|
success_fg: rotate_color(theme.flash.success_fg, degrees),
|
||||||
|
info_bg: rotate_color(theme.flash.info_bg, degrees),
|
||||||
|
info_fg: rotate_color(theme.flash.info_fg, degrees),
|
||||||
|
event_rgb: rotate_tuple(theme.flash.event_rgb, degrees),
|
||||||
|
},
|
||||||
|
list: ListColors {
|
||||||
|
playing_bg: rotate_color(theme.list.playing_bg, degrees),
|
||||||
|
playing_fg: rotate_color(theme.list.playing_fg, degrees),
|
||||||
|
staged_play_bg: rotate_color(theme.list.staged_play_bg, degrees),
|
||||||
|
staged_play_fg: rotate_color(theme.list.staged_play_fg, degrees),
|
||||||
|
staged_stop_bg: rotate_color(theme.list.staged_stop_bg, degrees),
|
||||||
|
staged_stop_fg: rotate_color(theme.list.staged_stop_fg, degrees),
|
||||||
|
edit_bg: rotate_color(theme.list.edit_bg, degrees),
|
||||||
|
edit_fg: rotate_color(theme.list.edit_fg, degrees),
|
||||||
|
hover_bg: rotate_color(theme.list.hover_bg, degrees),
|
||||||
|
hover_fg: rotate_color(theme.list.hover_fg, degrees),
|
||||||
|
},
|
||||||
|
link_status: LinkStatusColors {
|
||||||
|
disabled: rotate_color(theme.link_status.disabled, degrees),
|
||||||
|
connected: rotate_color(theme.link_status.connected, degrees),
|
||||||
|
listening: rotate_color(theme.link_status.listening, degrees),
|
||||||
|
},
|
||||||
|
syntax: SyntaxColors {
|
||||||
|
gap_bg: rotate_color(theme.syntax.gap_bg, degrees),
|
||||||
|
executed_bg: rotate_color(theme.syntax.executed_bg, degrees),
|
||||||
|
selected_bg: rotate_color(theme.syntax.selected_bg, degrees),
|
||||||
|
emit: rotate_color_pair(theme.syntax.emit, degrees),
|
||||||
|
number: rotate_color_pair(theme.syntax.number, degrees),
|
||||||
|
string: rotate_color_pair(theme.syntax.string, degrees),
|
||||||
|
comment: rotate_color_pair(theme.syntax.comment, degrees),
|
||||||
|
keyword: rotate_color_pair(theme.syntax.keyword, degrees),
|
||||||
|
stack_op: rotate_color_pair(theme.syntax.stack_op, degrees),
|
||||||
|
operator: rotate_color_pair(theme.syntax.operator, degrees),
|
||||||
|
sound: rotate_color_pair(theme.syntax.sound, degrees),
|
||||||
|
param: rotate_color_pair(theme.syntax.param, degrees),
|
||||||
|
context: rotate_color_pair(theme.syntax.context, degrees),
|
||||||
|
note: rotate_color_pair(theme.syntax.note, degrees),
|
||||||
|
interval: rotate_color_pair(theme.syntax.interval, degrees),
|
||||||
|
variable: rotate_color_pair(theme.syntax.variable, degrees),
|
||||||
|
vary: rotate_color_pair(theme.syntax.vary, degrees),
|
||||||
|
generator: rotate_color_pair(theme.syntax.generator, degrees),
|
||||||
|
default: rotate_color_pair(theme.syntax.default, degrees),
|
||||||
|
},
|
||||||
|
table: TableColors {
|
||||||
|
row_even: rotate_color(theme.table.row_even, degrees),
|
||||||
|
row_odd: rotate_color(theme.table.row_odd, degrees),
|
||||||
|
},
|
||||||
|
values: ValuesColors {
|
||||||
|
tempo: rotate_color(theme.values.tempo, degrees),
|
||||||
|
value: rotate_color(theme.values.value, degrees),
|
||||||
|
},
|
||||||
|
hint: HintColors {
|
||||||
|
key: rotate_color(theme.hint.key, degrees),
|
||||||
|
text: rotate_color(theme.hint.text, degrees),
|
||||||
|
},
|
||||||
|
view_badge: ViewBadgeColors {
|
||||||
|
bg: rotate_color(theme.view_badge.bg, degrees),
|
||||||
|
fg: rotate_color(theme.view_badge.fg, degrees),
|
||||||
|
},
|
||||||
|
nav: NavColors {
|
||||||
|
selected_bg: rotate_color(theme.nav.selected_bg, degrees),
|
||||||
|
selected_fg: rotate_color(theme.nav.selected_fg, degrees),
|
||||||
|
unselected_bg: rotate_color(theme.nav.unselected_bg, degrees),
|
||||||
|
unselected_fg: rotate_color(theme.nav.unselected_fg, degrees),
|
||||||
|
},
|
||||||
|
editor_widget: EditorWidgetColors {
|
||||||
|
cursor_bg: rotate_color(theme.editor_widget.cursor_bg, degrees),
|
||||||
|
cursor_fg: rotate_color(theme.editor_widget.cursor_fg, degrees),
|
||||||
|
selection_bg: rotate_color(theme.editor_widget.selection_bg, degrees),
|
||||||
|
completion_bg: rotate_color(theme.editor_widget.completion_bg, degrees),
|
||||||
|
completion_fg: rotate_color(theme.editor_widget.completion_fg, degrees),
|
||||||
|
completion_selected: rotate_color(theme.editor_widget.completion_selected, degrees),
|
||||||
|
completion_example: rotate_color(theme.editor_widget.completion_example, degrees),
|
||||||
|
},
|
||||||
|
browser: BrowserColors {
|
||||||
|
directory: rotate_color(theme.browser.directory, degrees),
|
||||||
|
project_file: rotate_color(theme.browser.project_file, degrees),
|
||||||
|
selected: rotate_color(theme.browser.selected, degrees),
|
||||||
|
file: rotate_color(theme.browser.file, degrees),
|
||||||
|
focused_border: rotate_color(theme.browser.focused_border, degrees),
|
||||||
|
unfocused_border: rotate_color(theme.browser.unfocused_border, degrees),
|
||||||
|
root: rotate_color(theme.browser.root, degrees),
|
||||||
|
file_icon: rotate_color(theme.browser.file_icon, degrees),
|
||||||
|
folder_icon: rotate_color(theme.browser.folder_icon, degrees),
|
||||||
|
empty_text: rotate_color(theme.browser.empty_text, degrees),
|
||||||
|
},
|
||||||
|
input: InputColors {
|
||||||
|
text: rotate_color(theme.input.text, degrees),
|
||||||
|
cursor: rotate_color(theme.input.cursor, degrees),
|
||||||
|
hint: rotate_color(theme.input.hint, degrees),
|
||||||
|
},
|
||||||
|
search: SearchColors {
|
||||||
|
active: rotate_color(theme.search.active, degrees),
|
||||||
|
inactive: rotate_color(theme.search.inactive, degrees),
|
||||||
|
match_bg: rotate_color(theme.search.match_bg, degrees),
|
||||||
|
match_fg: rotate_color(theme.search.match_fg, degrees),
|
||||||
|
},
|
||||||
|
markdown: MarkdownColors {
|
||||||
|
h1: rotate_color(theme.markdown.h1, degrees),
|
||||||
|
h2: rotate_color(theme.markdown.h2, degrees),
|
||||||
|
h3: rotate_color(theme.markdown.h3, degrees),
|
||||||
|
code: rotate_color(theme.markdown.code, degrees),
|
||||||
|
code_border: rotate_color(theme.markdown.code_border, degrees),
|
||||||
|
link: rotate_color(theme.markdown.link, degrees),
|
||||||
|
link_url: rotate_color(theme.markdown.link_url, degrees),
|
||||||
|
quote: rotate_color(theme.markdown.quote, degrees),
|
||||||
|
text: rotate_color(theme.markdown.text, degrees),
|
||||||
|
list: rotate_color(theme.markdown.list, degrees),
|
||||||
|
},
|
||||||
|
engine: EngineColors {
|
||||||
|
header: rotate_color(theme.engine.header, degrees),
|
||||||
|
header_focused: rotate_color(theme.engine.header_focused, degrees),
|
||||||
|
divider: rotate_color(theme.engine.divider, degrees),
|
||||||
|
scroll_indicator: rotate_color(theme.engine.scroll_indicator, degrees),
|
||||||
|
label: rotate_color(theme.engine.label, degrees),
|
||||||
|
label_focused: rotate_color(theme.engine.label_focused, degrees),
|
||||||
|
label_dim: rotate_color(theme.engine.label_dim, degrees),
|
||||||
|
value: rotate_color(theme.engine.value, degrees),
|
||||||
|
focused: rotate_color(theme.engine.focused, degrees),
|
||||||
|
normal: rotate_color(theme.engine.normal, degrees),
|
||||||
|
dim: rotate_color(theme.engine.dim, degrees),
|
||||||
|
path: rotate_color(theme.engine.path, degrees),
|
||||||
|
border_magenta: rotate_color(theme.engine.border_magenta, degrees),
|
||||||
|
border_green: rotate_color(theme.engine.border_green, degrees),
|
||||||
|
border_cyan: rotate_color(theme.engine.border_cyan, degrees),
|
||||||
|
separator: rotate_color(theme.engine.separator, degrees),
|
||||||
|
hint_active: rotate_color(theme.engine.hint_active, degrees),
|
||||||
|
hint_inactive: rotate_color(theme.engine.hint_inactive, degrees),
|
||||||
|
},
|
||||||
|
dict: DictColors {
|
||||||
|
word_name: rotate_color(theme.dict.word_name, degrees),
|
||||||
|
word_bg: rotate_color(theme.dict.word_bg, degrees),
|
||||||
|
alias: rotate_color(theme.dict.alias, degrees),
|
||||||
|
stack_sig: rotate_color(theme.dict.stack_sig, degrees),
|
||||||
|
description: rotate_color(theme.dict.description, degrees),
|
||||||
|
example: rotate_color(theme.dict.example, degrees),
|
||||||
|
category_focused: rotate_color(theme.dict.category_focused, degrees),
|
||||||
|
category_selected: rotate_color(theme.dict.category_selected, degrees),
|
||||||
|
category_normal: rotate_color(theme.dict.category_normal, degrees),
|
||||||
|
category_dimmed: rotate_color(theme.dict.category_dimmed, degrees),
|
||||||
|
border_focused: rotate_color(theme.dict.border_focused, degrees),
|
||||||
|
border_normal: rotate_color(theme.dict.border_normal, degrees),
|
||||||
|
header_desc: rotate_color(theme.dict.header_desc, degrees),
|
||||||
|
},
|
||||||
|
title: TitleColors {
|
||||||
|
big_title: rotate_color(theme.title.big_title, degrees),
|
||||||
|
author: rotate_color(theme.title.author, degrees),
|
||||||
|
link: rotate_color(theme.title.link, degrees),
|
||||||
|
license: rotate_color(theme.title.license, degrees),
|
||||||
|
prompt: rotate_color(theme.title.prompt, degrees),
|
||||||
|
subtitle: rotate_color(theme.title.subtitle, degrees),
|
||||||
|
},
|
||||||
|
meter: MeterColors {
|
||||||
|
low: rotate_color(theme.meter.low, degrees),
|
||||||
|
mid: rotate_color(theme.meter.mid, degrees),
|
||||||
|
high: rotate_color(theme.meter.high, degrees),
|
||||||
|
low_rgb: rotate_tuple(theme.meter.low_rgb, degrees),
|
||||||
|
mid_rgb: rotate_tuple(theme.meter.mid_rgb, degrees),
|
||||||
|
high_rgb: rotate_tuple(theme.meter.high_rgb, degrees),
|
||||||
|
},
|
||||||
|
sparkle: SparkleColors {
|
||||||
|
colors: [
|
||||||
|
rotate_tuple(theme.sparkle.colors[0], degrees),
|
||||||
|
rotate_tuple(theme.sparkle.colors[1], degrees),
|
||||||
|
rotate_tuple(theme.sparkle.colors[2], degrees),
|
||||||
|
rotate_tuple(theme.sparkle.colors[3], degrees),
|
||||||
|
rotate_tuple(theme.sparkle.colors[4], degrees),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
confirm: ConfirmColors {
|
||||||
|
border: rotate_color(theme.confirm.border, degrees),
|
||||||
|
button_selected_bg: rotate_color(theme.confirm.button_selected_bg, degrees),
|
||||||
|
button_selected_fg: rotate_color(theme.confirm.button_selected_fg, degrees),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/app.rs
88
src/app.rs
@@ -111,6 +111,7 @@ impl App {
|
|||||||
flash_brightness: self.ui.flash_brightness,
|
flash_brightness: self.ui.flash_brightness,
|
||||||
color_scheme: self.ui.color_scheme,
|
color_scheme: self.ui.color_scheme,
|
||||||
layout: self.audio.config.layout,
|
layout: self.audio.config.layout,
|
||||||
|
hue_rotation: self.ui.hue_rotation,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
link: crate::settings::LinkSettings {
|
link: crate::settings::LinkSettings {
|
||||||
@@ -1311,7 +1312,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppCommand::SetColorScheme(scheme) => {
|
AppCommand::SetColorScheme(scheme) => {
|
||||||
self.ui.color_scheme = scheme;
|
self.ui.color_scheme = scheme;
|
||||||
crate::theme::set(scheme.to_theme());
|
let base_theme = scheme.to_theme();
|
||||||
|
let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, self.ui.hue_rotation);
|
||||||
|
crate::theme::set(rotated);
|
||||||
|
}
|
||||||
|
AppCommand::SetHueRotation(degrees) => {
|
||||||
|
self.ui.hue_rotation = degrees;
|
||||||
|
let base_theme = self.ui.color_scheme.to_theme();
|
||||||
|
let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, degrees);
|
||||||
|
crate::theme::set(rotated);
|
||||||
}
|
}
|
||||||
AppCommand::ToggleRuntimeHighlight => {
|
AppCommand::ToggleRuntimeHighlight => {
|
||||||
self.ui.runtime_highlight = !self.ui.runtime_highlight;
|
self.ui.runtime_highlight = !self.ui.runtime_highlight;
|
||||||
@@ -1429,6 +1438,65 @@ impl App {
|
|||||||
AppCommand::ResetPeakVoices => {
|
AppCommand::ResetPeakVoices => {
|
||||||
self.metrics.peak_voices = 0;
|
self.metrics.peak_voices = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Euclidean distribution
|
||||||
|
AppCommand::ApplyEuclideanDistribution {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
source_step,
|
||||||
|
pulses,
|
||||||
|
steps,
|
||||||
|
rotation,
|
||||||
|
} => {
|
||||||
|
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
||||||
|
let rhythm = euclidean_rhythm(pulses, steps, rotation);
|
||||||
|
|
||||||
|
let mut created_count = 0;
|
||||||
|
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||||
|
if !is_hit {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = (source_step + i) % pat_len;
|
||||||
|
|
||||||
|
if target == source_step {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(step) = self
|
||||||
|
.project_state
|
||||||
|
.project
|
||||||
|
.pattern_at_mut(bank, pattern)
|
||||||
|
.step_mut(target)
|
||||||
|
{
|
||||||
|
step.source = Some(source_step);
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
step.active = true;
|
||||||
|
}
|
||||||
|
created_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
|
||||||
|
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||||
|
if !is_hit || i == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let target = (source_step + i) % pat_len;
|
||||||
|
let saved = self.editor_ctx.step;
|
||||||
|
self.editor_ctx.step = target;
|
||||||
|
self.compile_current_step(link);
|
||||||
|
self.editor_ctx.step = saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.load_step_to_editor();
|
||||||
|
self.ui.flash(
|
||||||
|
&format!("Created {} linked steps (E({pulses},{steps}))", created_count),
|
||||||
|
200,
|
||||||
|
FlashKind::Success,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1481,3 +1549,21 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn euclidean_rhythm(pulses: usize, steps: usize, rotation: usize) -> Vec<bool> {
|
||||||
|
if pulses == 0 || steps == 0 || pulses > steps {
|
||||||
|
return vec![false; steps];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pattern = vec![false; steps];
|
||||||
|
for i in 0..pulses {
|
||||||
|
let pos = (i * steps) / pulses;
|
||||||
|
pattern[pos] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rotation > 0 {
|
||||||
|
pattern.rotate_left(rotation % steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ pub enum AppCommand {
|
|||||||
HideTitle,
|
HideTitle,
|
||||||
ToggleEditorStack,
|
ToggleEditorStack,
|
||||||
SetColorScheme(ColorScheme),
|
SetColorScheme(ColorScheme),
|
||||||
|
SetHueRotation(f32),
|
||||||
ToggleRuntimeHighlight,
|
ToggleRuntimeHighlight,
|
||||||
ToggleCompletion,
|
ToggleCompletion,
|
||||||
AdjustFlashBrightness(f32),
|
AdjustFlashBrightness(f32),
|
||||||
@@ -207,4 +208,13 @@ pub enum AppCommand {
|
|||||||
// Metrics
|
// Metrics
|
||||||
ResetPeakVoices,
|
ResetPeakVoices,
|
||||||
|
|
||||||
|
// Euclidean distribution
|
||||||
|
ApplyEuclideanDistribution {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
source_step: usize,
|
||||||
|
pulses: usize,
|
||||||
|
steps: usize,
|
||||||
|
rotation: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/input.rs
101
src/input.rs
@@ -11,8 +11,8 @@ use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
|||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CyclicEnum, DeviceKind, EngineSection, Modal, OptionsFocus, PanelFocus, PatternField,
|
CyclicEnum, DeviceKind, EngineSection, EuclideanField, Modal, OptionsFocus, PanelFocus,
|
||||||
PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
@@ -641,6 +641,79 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Modal::EuclideanDistribution {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
source_step,
|
||||||
|
field,
|
||||||
|
pulses,
|
||||||
|
steps,
|
||||||
|
rotation,
|
||||||
|
} => {
|
||||||
|
let (bank_val, pattern_val, source_step_val) = (*bank, *pattern, *source_step);
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => *field = field.prev(),
|
||||||
|
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||||
|
KeyCode::Left => {
|
||||||
|
let target = match field {
|
||||||
|
EuclideanField::Pulses => pulses,
|
||||||
|
EuclideanField::Steps => steps,
|
||||||
|
EuclideanField::Rotation => rotation,
|
||||||
|
};
|
||||||
|
if let Ok(val) = target.parse::<usize>() {
|
||||||
|
*target = val.saturating_sub(1).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
let target = match field {
|
||||||
|
EuclideanField::Pulses => pulses,
|
||||||
|
EuclideanField::Steps => steps,
|
||||||
|
EuclideanField::Rotation => rotation,
|
||||||
|
};
|
||||||
|
if let Ok(val) = target.parse::<usize>() {
|
||||||
|
*target = (val + 1).min(128).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if c.is_ascii_digit() => match field {
|
||||||
|
EuclideanField::Pulses => pulses.push(c),
|
||||||
|
EuclideanField::Steps => steps.push(c),
|
||||||
|
EuclideanField::Rotation => rotation.push(c),
|
||||||
|
},
|
||||||
|
KeyCode::Backspace => match field {
|
||||||
|
EuclideanField::Pulses => {
|
||||||
|
pulses.pop();
|
||||||
|
}
|
||||||
|
EuclideanField::Steps => {
|
||||||
|
steps.pop();
|
||||||
|
}
|
||||||
|
EuclideanField::Rotation => {
|
||||||
|
rotation.pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let pulses_val: usize = pulses.parse().unwrap_or(0);
|
||||||
|
let steps_val: usize = steps.parse().unwrap_or(0);
|
||||||
|
let rotation_val: usize = rotation.parse().unwrap_or(0);
|
||||||
|
if pulses_val > 0 && steps_val > 0 && pulses_val <= steps_val {
|
||||||
|
ctx.dispatch(AppCommand::ApplyEuclideanDistribution {
|
||||||
|
bank: bank_val,
|
||||||
|
pattern: pattern_val,
|
||||||
|
source_step: source_step_val,
|
||||||
|
pulses: pulses_val,
|
||||||
|
steps: steps_val,
|
||||||
|
rotation: rotation_val,
|
||||||
|
});
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
} else {
|
||||||
|
ctx.dispatch(AppCommand::SetStatus(
|
||||||
|
"Invalid: pulses must be > 0 and <= steps".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Modal::None => unreachable!(),
|
Modal::None => unreachable!(),
|
||||||
}
|
}
|
||||||
InputResult::Continue
|
InputResult::Continue
|
||||||
@@ -962,6 +1035,25 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('e') | KeyCode::Char('E') => {
|
||||||
|
let (bank, pattern, step) = (
|
||||||
|
ctx.app.editor_ctx.bank,
|
||||||
|
ctx.app.editor_ctx.pattern,
|
||||||
|
ctx.app.editor_ctx.step,
|
||||||
|
);
|
||||||
|
let pattern_len = ctx.app.current_edit_pattern().length;
|
||||||
|
let default_steps = pattern_len.min(32);
|
||||||
|
let default_pulses = (default_steps / 2).max(1).min(default_steps);
|
||||||
|
ctx.dispatch(AppCommand::OpenModal(Modal::EuclideanDistribution {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
source_step: step,
|
||||||
|
field: EuclideanField::Pulses,
|
||||||
|
pulses: default_pulses.to_string(),
|
||||||
|
steps: default_steps.to_string(),
|
||||||
|
rotation: "0".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
InputResult::Continue
|
InputResult::Continue
|
||||||
@@ -1292,6 +1384,11 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
};
|
};
|
||||||
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
|
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
|
||||||
}
|
}
|
||||||
|
OptionsFocus::HueRotation => {
|
||||||
|
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
|
||||||
|
let new_rotation = (ctx.app.ui.hue_rotation + delta).rem_euclid(360.0);
|
||||||
|
ctx.dispatch(AppCommand::SetHueRotation(new_rotation));
|
||||||
|
}
|
||||||
OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate),
|
OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate),
|
||||||
OptionsFocus::RuntimeHighlight => {
|
OptionsFocus::RuntimeHighlight => {
|
||||||
ctx.dispatch(AppCommand::ToggleRuntimeHighlight);
|
ctx.dispatch(AppCommand::ToggleRuntimeHighlight);
|
||||||
|
|||||||
@@ -100,8 +100,11 @@ fn main() -> io::Result<()> {
|
|||||||
app.ui.show_completion = settings.display.show_completion;
|
app.ui.show_completion = settings.display.show_completion;
|
||||||
app.ui.flash_brightness = settings.display.flash_brightness;
|
app.ui.flash_brightness = settings.display.flash_brightness;
|
||||||
app.ui.color_scheme = settings.display.color_scheme;
|
app.ui.color_scheme = settings.display.color_scheme;
|
||||||
|
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||||
app.audio.config.layout = settings.display.layout;
|
app.audio.config.layout = settings.display.layout;
|
||||||
theme::set(settings.display.color_scheme.to_theme());
|
let base_theme = settings.display.color_scheme.to_theme();
|
||||||
|
let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation);
|
||||||
|
theme::set(rotated);
|
||||||
|
|
||||||
// Load MIDI settings
|
// Load MIDI settings
|
||||||
let outputs = midi::list_midi_outputs();
|
let outputs = midi::list_midi_outputs();
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ pub struct DisplaySettings {
|
|||||||
pub color_scheme: ColorScheme,
|
pub color_scheme: ColorScheme,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub layout: MainLayout,
|
pub layout: MainLayout,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hue_rotation: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_font() -> String {
|
fn default_font() -> String {
|
||||||
@@ -94,6 +96,7 @@ impl Default for DisplaySettings {
|
|||||||
font: default_font(),
|
font: default_font(),
|
||||||
color_scheme: ColorScheme::default(),
|
color_scheme: ColorScheme::default(),
|
||||||
layout: MainLayout::default(),
|
layout: MainLayout::default(),
|
||||||
|
hue_rotation: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,32 @@ impl PatternPropsField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum EuclideanField {
|
||||||
|
#[default]
|
||||||
|
Pulses,
|
||||||
|
Steps,
|
||||||
|
Rotation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EuclideanField {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Pulses => Self::Steps,
|
||||||
|
Self::Steps => Self::Rotation,
|
||||||
|
Self::Rotation => Self::Rotation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Pulses => Self::Pulses,
|
||||||
|
Self::Steps => Self::Pulses,
|
||||||
|
Self::Rotation => Self::Steps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EditorContext {
|
pub struct EditorContext {
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
pub pattern: usize,
|
pub pattern: usize,
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ pub mod ui;
|
|||||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||||
pub use color_scheme::ColorScheme;
|
pub use color_scheme::ColorScheme;
|
||||||
pub use editor::{
|
pub use editor::{
|
||||||
CopiedStepData, CopiedSteps, EditorContext, PatternField, PatternPropsField, StackCache,
|
CopiedStepData, CopiedSteps, EditorContext, EuclideanField, PatternField, PatternPropsField,
|
||||||
|
StackCache,
|
||||||
};
|
};
|
||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::Modal;
|
pub use modal::Modal;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
use crate::model::{LaunchQuantization, PatternSpeed, SyncMode};
|
||||||
use crate::state::editor::{PatternField, PatternPropsField};
|
use crate::state::editor::{EuclideanField, PatternField, PatternPropsField};
|
||||||
use crate::state::file_browser::FileBrowserState;
|
use crate::state::file_browser::FileBrowserState;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
@@ -66,4 +66,13 @@ pub enum Modal {
|
|||||||
KeybindingsHelp {
|
KeybindingsHelp {
|
||||||
scroll: usize,
|
scroll: usize,
|
||||||
},
|
},
|
||||||
|
EuclideanDistribution {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
source_step: usize,
|
||||||
|
field: EuclideanField,
|
||||||
|
pulses: String,
|
||||||
|
steps: String,
|
||||||
|
rotation: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use super::CyclicEnum;
|
|||||||
pub enum OptionsFocus {
|
pub enum OptionsFocus {
|
||||||
#[default]
|
#[default]
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
|
HueRotation,
|
||||||
RefreshRate,
|
RefreshRate,
|
||||||
RuntimeHighlight,
|
RuntimeHighlight,
|
||||||
ShowScope,
|
ShowScope,
|
||||||
@@ -26,6 +27,7 @@ pub enum OptionsFocus {
|
|||||||
impl CyclicEnum for OptionsFocus {
|
impl CyclicEnum for OptionsFocus {
|
||||||
const VARIANTS: &'static [Self] = &[
|
const VARIANTS: &'static [Self] = &[
|
||||||
Self::ColorScheme,
|
Self::ColorScheme,
|
||||||
|
Self::HueRotation,
|
||||||
Self::RefreshRate,
|
Self::RefreshRate,
|
||||||
Self::RuntimeHighlight,
|
Self::RuntimeHighlight,
|
||||||
Self::ShowScope,
|
Self::ShowScope,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub struct UiState {
|
|||||||
pub event_flash: f32,
|
pub event_flash: f32,
|
||||||
pub flash_brightness: f32,
|
pub flash_brightness: f32,
|
||||||
pub color_scheme: ColorScheme,
|
pub color_scheme: ColorScheme,
|
||||||
|
pub hue_rotation: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -78,6 +79,7 @@ impl Default for UiState {
|
|||||||
event_flash: 0.0,
|
event_flash: 0.0,
|
||||||
flash_brightness: 1.0,
|
flash_brightness: 1.0,
|
||||||
color_scheme: ColorScheme::default(),
|
color_scheme: ColorScheme::default(),
|
||||||
|
hue_rotation: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
|||||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||||
bindings.push(("r", "Rename", "Rename current step"));
|
bindings.push(("r", "Rename", "Rename current step"));
|
||||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||||
|
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
||||||
}
|
}
|
||||||
Page::Patterns => {
|
Page::Patterns => {
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||||
|
|||||||
@@ -136,22 +136,15 @@ fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot,
|
|||||||
let num_rows = steps_on_page.div_ceil(8);
|
let num_rows = steps_on_page.div_ceil(8);
|
||||||
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
let steps_per_row = steps_on_page.div_ceil(num_rows);
|
||||||
|
|
||||||
let spacing = num_rows.saturating_sub(1) as u16;
|
let row_height = area.height / num_rows as u16;
|
||||||
let row_height = area.height.saturating_sub(spacing) / num_rows as u16;
|
|
||||||
|
|
||||||
let row_constraints: Vec<Constraint> = (0..num_rows * 2 - 1)
|
let row_constraints: Vec<Constraint> = (0..num_rows)
|
||||||
.map(|i| {
|
.map(|_| Constraint::Length(row_height))
|
||||||
if i % 2 == 0 {
|
|
||||||
Constraint::Length(row_height)
|
|
||||||
} else {
|
|
||||||
Constraint::Length(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let rows = Layout::vertical(row_constraints).split(area);
|
let rows = Layout::vertical(row_constraints).split(area);
|
||||||
|
|
||||||
for row_idx in 0..num_rows {
|
for row_idx in 0..num_rows {
|
||||||
let row_area = rows[row_idx * 2];
|
let row_area = rows[row_idx];
|
||||||
let start_step = row_idx * steps_per_row;
|
let start_step = row_idx * steps_per_row;
|
||||||
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
let end_step = (start_step + steps_per_row).min(steps_on_page);
|
||||||
let cols_in_row = end_step - start_step;
|
let cols_in_row = end_step - start_step;
|
||||||
@@ -226,6 +219,12 @@ fn render_tile(
|
|||||||
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
|
(false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(theme.ui.border));
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let source_idx = step.and_then(|s| s.source);
|
let source_idx = step.and_then(|s| s.source);
|
||||||
let symbol = if is_playing {
|
let symbol = if is_playing {
|
||||||
"▶".to_string()
|
"▶".to_string()
|
||||||
@@ -243,17 +242,17 @@ fn render_tile(
|
|||||||
};
|
};
|
||||||
let num_lines = if step_name.is_some() { 2u16 } else { 1u16 };
|
let num_lines = if step_name.is_some() { 2u16 } else { 1u16 };
|
||||||
let content_height = num_lines;
|
let content_height = num_lines;
|
||||||
let y_offset = area.height.saturating_sub(content_height) / 2;
|
let y_offset = inner.height.saturating_sub(content_height) / 2;
|
||||||
|
|
||||||
// Fill background for entire tile
|
// Fill background for inner area
|
||||||
let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
|
let bg_fill = Paragraph::new("").style(Style::new().bg(bg));
|
||||||
frame.render_widget(bg_fill, area);
|
frame.render_widget(bg_fill, inner);
|
||||||
|
|
||||||
if let Some(name) = step_name {
|
if let Some(name) = step_name {
|
||||||
let name_area = Rect {
|
let name_area = Rect {
|
||||||
x: area.x,
|
x: inner.x,
|
||||||
y: area.y + y_offset,
|
y: inner.y + y_offset,
|
||||||
width: area.width,
|
width: inner.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let name_widget = Paragraph::new(name.as_str())
|
let name_widget = Paragraph::new(name.as_str())
|
||||||
@@ -262,9 +261,9 @@ fn render_tile(
|
|||||||
frame.render_widget(name_widget, name_area);
|
frame.render_widget(name_widget, name_area);
|
||||||
|
|
||||||
let symbol_area = Rect {
|
let symbol_area = Rect {
|
||||||
x: area.x,
|
x: inner.x,
|
||||||
y: area.y + y_offset + 1,
|
y: inner.y + y_offset + 1,
|
||||||
width: area.width,
|
width: inner.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let symbol_widget = Paragraph::new(symbol)
|
let symbol_widget = Paragraph::new(symbol)
|
||||||
@@ -273,9 +272,9 @@ fn render_tile(
|
|||||||
frame.render_widget(symbol_widget, symbol_area);
|
frame.render_widget(symbol_widget, symbol_area);
|
||||||
} else {
|
} else {
|
||||||
let centered_area = Rect {
|
let centered_area = Rect {
|
||||||
x: area.x,
|
x: inner.x,
|
||||||
y: area.y + y_offset,
|
y: inner.y + y_offset,
|
||||||
width: area.width,
|
width: inner.width,
|
||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
let tile = Paragraph::new(symbol)
|
let tile = Paragraph::new(symbol)
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
let midi_in_2 = midi_in_display(2);
|
let midi_in_2 = midi_in_display(2);
|
||||||
let midi_in_3 = midi_in_display(3);
|
let midi_in_3 = midi_in_display(3);
|
||||||
|
|
||||||
|
let hue_str = format!("{}°", app.ui.hue_rotation as i32);
|
||||||
|
|
||||||
let lines: Vec<Line> = vec![
|
let lines: Vec<Line> = vec![
|
||||||
render_section_header("DISPLAY", &theme),
|
render_section_header("DISPLAY", &theme),
|
||||||
render_divider(content_width, &theme),
|
render_divider(content_width, &theme),
|
||||||
@@ -119,6 +121,12 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
focus == OptionsFocus::ColorScheme,
|
focus == OptionsFocus::ColorScheme,
|
||||||
&theme,
|
&theme,
|
||||||
),
|
),
|
||||||
|
render_option_line(
|
||||||
|
"Hue rotation",
|
||||||
|
&hue_str,
|
||||||
|
focus == OptionsFocus::HueRotation,
|
||||||
|
&theme,
|
||||||
|
),
|
||||||
render_option_line(
|
render_option_line(
|
||||||
"Refresh rate",
|
"Refresh rate",
|
||||||
app.audio.config.refresh_rate.label(),
|
app.audio.config.refresh_rate.label(),
|
||||||
@@ -201,23 +209,24 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
|
|
||||||
let focus_line: usize = match focus {
|
let focus_line: usize = match focus {
|
||||||
OptionsFocus::ColorScheme => 2,
|
OptionsFocus::ColorScheme => 2,
|
||||||
OptionsFocus::RefreshRate => 3,
|
OptionsFocus::HueRotation => 3,
|
||||||
OptionsFocus::RuntimeHighlight => 4,
|
OptionsFocus::RefreshRate => 4,
|
||||||
OptionsFocus::ShowScope => 5,
|
OptionsFocus::RuntimeHighlight => 5,
|
||||||
OptionsFocus::ShowSpectrum => 6,
|
OptionsFocus::ShowScope => 6,
|
||||||
OptionsFocus::ShowCompletion => 7,
|
OptionsFocus::ShowSpectrum => 7,
|
||||||
OptionsFocus::FlashBrightness => 8,
|
OptionsFocus::ShowCompletion => 8,
|
||||||
OptionsFocus::LinkEnabled => 12,
|
OptionsFocus::FlashBrightness => 9,
|
||||||
OptionsFocus::StartStopSync => 13,
|
OptionsFocus::LinkEnabled => 13,
|
||||||
OptionsFocus::Quantum => 14,
|
OptionsFocus::StartStopSync => 14,
|
||||||
OptionsFocus::MidiOutput0 => 25,
|
OptionsFocus::Quantum => 15,
|
||||||
OptionsFocus::MidiOutput1 => 26,
|
OptionsFocus::MidiOutput0 => 26,
|
||||||
OptionsFocus::MidiOutput2 => 27,
|
OptionsFocus::MidiOutput1 => 27,
|
||||||
OptionsFocus::MidiOutput3 => 28,
|
OptionsFocus::MidiOutput2 => 28,
|
||||||
OptionsFocus::MidiInput0 => 32,
|
OptionsFocus::MidiOutput3 => 29,
|
||||||
OptionsFocus::MidiInput1 => 33,
|
OptionsFocus::MidiInput0 => 33,
|
||||||
OptionsFocus::MidiInput2 => 34,
|
OptionsFocus::MidiInput1 => 34,
|
||||||
OptionsFocus::MidiInput3 => 35,
|
OptionsFocus::MidiInput2 => 35,
|
||||||
|
OptionsFocus::MidiInput3 => 36,
|
||||||
};
|
};
|
||||||
|
|
||||||
let scroll_offset = if total_lines <= max_visible {
|
let scroll_offset = if total_lines <= max_visible {
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ use crate::app::App;
|
|||||||
use crate::engine::{LinkState, SequencerSnapshot};
|
use crate::engine::{LinkState, SequencerSnapshot};
|
||||||
use crate::model::{SourceSpan, StepContext, Value};
|
use crate::model::{SourceSpan, StepContext, Value};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache};
|
use crate::state::{
|
||||||
|
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache,
|
||||||
|
};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
@@ -1038,5 +1040,131 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
hint_area,
|
hint_area,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Modal::EuclideanDistribution {
|
||||||
|
source_step,
|
||||||
|
field,
|
||||||
|
pulses,
|
||||||
|
steps,
|
||||||
|
rotation,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let width = 50u16;
|
||||||
|
let height = 11u16;
|
||||||
|
let x = (term.width.saturating_sub(width)) / 2;
|
||||||
|
let y = (term.height.saturating_sub(height)) / 2;
|
||||||
|
let area = Rect::new(x, y, width, height);
|
||||||
|
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(format!(" Euclidean Distribution (Step {:02}) ", source_step + 1))
|
||||||
|
.border_style(Style::default().fg(theme.modal.input));
|
||||||
|
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
// Fill background with theme color
|
||||||
|
let bg_fill = " ".repeat(area.width as usize);
|
||||||
|
for row in 0..area.height {
|
||||||
|
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(bg_fill.clone()).style(Style::new().bg(theme.ui.bg)),
|
||||||
|
line_area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let fields = [
|
||||||
|
(
|
||||||
|
"Pulses",
|
||||||
|
pulses.as_str(),
|
||||||
|
*field == EuclideanField::Pulses,
|
||||||
|
),
|
||||||
|
("Steps", steps.as_str(), *field == EuclideanField::Steps),
|
||||||
|
(
|
||||||
|
"Rotation",
|
||||||
|
rotation.as_str(),
|
||||||
|
*field == EuclideanField::Rotation,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, (label, value, selected)) in fields.iter().enumerate() {
|
||||||
|
let row_y = inner.y + i as u16;
|
||||||
|
if row_y >= inner.y + inner.height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (label_style, value_style) = if *selected {
|
||||||
|
(
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.hint.key)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.ui.text_primary)
|
||||||
|
.bg(theme.ui.surface),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Style::default().fg(theme.ui.text_muted),
|
||||||
|
Style::default().fg(theme.ui.text_primary),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let label_area = Rect::new(inner.x + 1, row_y, 14, 1);
|
||||||
|
let value_area = Rect::new(inner.x + 16, row_y, inner.width.saturating_sub(18), 1);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(format!("{label}:")).style(label_style),
|
||||||
|
label_area,
|
||||||
|
);
|
||||||
|
frame.render_widget(Paragraph::new(*value).style(value_style), value_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview_y = inner.y + 4;
|
||||||
|
if preview_y < inner.y + inner.height {
|
||||||
|
let pulses_val: usize = pulses.parse().unwrap_or(0);
|
||||||
|
let steps_val: usize = steps.parse().unwrap_or(0);
|
||||||
|
let rotation_val: usize = rotation.parse().unwrap_or(0);
|
||||||
|
let preview = format_euclidean_preview(pulses_val, steps_val, rotation_val);
|
||||||
|
let preview_line = Line::from(vec![
|
||||||
|
Span::styled("Preview: ", Style::default().fg(theme.ui.text_muted)),
|
||||||
|
Span::styled(preview, Style::default().fg(theme.modal.input)),
|
||||||
|
]);
|
||||||
|
let preview_area =
|
||||||
|
Rect::new(inner.x + 1, preview_y, inner.width.saturating_sub(2), 1);
|
||||||
|
frame.render_widget(Paragraph::new(preview_line), preview_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
|
||||||
|
let hint_line = Line::from(vec![
|
||||||
|
Span::styled("↑↓", Style::default().fg(theme.hint.key)),
|
||||||
|
Span::styled(" nav ", Style::default().fg(theme.hint.text)),
|
||||||
|
Span::styled("←→", Style::default().fg(theme.hint.key)),
|
||||||
|
Span::styled(" adjust ", Style::default().fg(theme.hint.text)),
|
||||||
|
Span::styled("Enter", Style::default().fg(theme.hint.key)),
|
||||||
|
Span::styled(" apply ", Style::default().fg(theme.hint.text)),
|
||||||
|
Span::styled("Esc", Style::default().fg(theme.hint.key)),
|
||||||
|
Span::styled(" cancel", Style::default().fg(theme.hint.text)),
|
||||||
|
]);
|
||||||
|
frame.render_widget(Paragraph::new(hint_line), hint_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_euclidean_preview(pulses: usize, steps: usize, rotation: usize) -> String {
|
||||||
|
if pulses == 0 || steps == 0 || pulses > steps {
|
||||||
|
return "[invalid]".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pattern = vec![false; steps];
|
||||||
|
for i in 0..pulses {
|
||||||
|
let pos = (i * steps) / pulses;
|
||||||
|
pattern[pos] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rotation > 0 {
|
||||||
|
pattern.rotate_left(rotation % steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
let chars: Vec<&str> = pattern.iter().map(|&h| if h { "x" } else { "." }).collect();
|
||||||
|
format!("[{}]", chars.join(" "))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user