This commit is contained in:
@@ -214,6 +214,11 @@ pub fn create_editor(
|
|||||||
let shared = editor.bridge.shared_state.load();
|
let shared = editor.bridge.shared_state.load();
|
||||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||||
|
|
||||||
|
// Sync host tempo into LinkState so title bar shows real tempo
|
||||||
|
if shared.tempo > 0.0 {
|
||||||
|
editor.link.set_tempo(shared.tempo);
|
||||||
|
}
|
||||||
|
|
||||||
// Feed scope and spectrum data into app metrics
|
// Feed scope and spectrum data into app metrics
|
||||||
editor.app.metrics.scope = editor.bridge.scope_buffer.read();
|
editor.app.metrics.scope = editor.bridge.scope_buffer.read();
|
||||||
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) =
|
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) =
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ impl App {
|
|||||||
Self::build(variables, dict, rng, false)
|
Self::build(variables, dict, rng, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
pub fn new_plugin(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||||
Self::build(variables, dict, rng, true)
|
Self::build(variables, dict, rng, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,12 +158,17 @@ pub struct SharedSequencerState {
|
|||||||
pub active_patterns: Vec<ActivePatternState>,
|
pub active_patterns: Vec<ActivePatternState>,
|
||||||
pub step_traces: Arc<StepTracesMap>,
|
pub step_traces: Arc<StepTracesMap>,
|
||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
|
pub tempo: f64,
|
||||||
|
pub beat: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct SequencerSnapshot {
|
pub struct SequencerSnapshot {
|
||||||
pub active_patterns: Vec<ActivePatternState>,
|
pub active_patterns: Vec<ActivePatternState>,
|
||||||
step_traces: Arc<StepTracesMap>,
|
step_traces: Arc<StepTracesMap>,
|
||||||
pub event_count: usize,
|
pub event_count: usize,
|
||||||
|
pub tempo: f64,
|
||||||
|
pub beat: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&SharedSequencerState> for SequencerSnapshot {
|
impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||||
@@ -172,6 +177,8 @@ impl From<&SharedSequencerState> for SequencerSnapshot {
|
|||||||
active_patterns: s.active_patterns.clone(),
|
active_patterns: s.active_patterns.clone(),
|
||||||
step_traces: Arc::clone(&s.step_traces),
|
step_traces: Arc::clone(&s.step_traces),
|
||||||
event_count: s.event_count,
|
event_count: s.event_count,
|
||||||
|
tempo: s.tempo,
|
||||||
|
beat: s.beat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +190,8 @@ impl SequencerSnapshot {
|
|||||||
active_patterns: Vec::new(),
|
active_patterns: Vec::new(),
|
||||||
step_traces: Arc::new(HashMap::new()),
|
step_traces: Arc::new(HashMap::new()),
|
||||||
event_count: 0,
|
event_count: 0,
|
||||||
|
tempo: 0.0,
|
||||||
|
beat: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,11 +231,7 @@ pub struct SequencerHandle {
|
|||||||
impl SequencerHandle {
|
impl SequencerHandle {
|
||||||
pub fn snapshot(&self) -> SequencerSnapshot {
|
pub fn snapshot(&self) -> SequencerSnapshot {
|
||||||
let state = self.shared_state.load();
|
let state = self.shared_state.load();
|
||||||
SequencerSnapshot {
|
SequencerSnapshot::from(state.as_ref())
|
||||||
active_patterns: state.active_patterns.clone(),
|
|
||||||
step_traces: Arc::clone(&state.step_traces),
|
|
||||||
event_count: state.event_count,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
|
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
|
||||||
@@ -563,6 +568,8 @@ pub struct SequencerState {
|
|||||||
cc_access: Option<Arc<dyn CcAccess>>,
|
cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
muted: std::collections::HashSet<(usize, usize)>,
|
muted: std::collections::HashSet<(usize, usize)>,
|
||||||
soloed: std::collections::HashSet<(usize, usize)>,
|
soloed: std::collections::HashSet<(usize, usize)>,
|
||||||
|
last_tempo: f64,
|
||||||
|
last_beat: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SequencerState {
|
impl SequencerState {
|
||||||
@@ -592,6 +599,8 @@ impl SequencerState {
|
|||||||
cc_access,
|
cc_access,
|
||||||
muted: std::collections::HashSet::new(),
|
muted: std::collections::HashSet::new(),
|
||||||
soloed: std::collections::HashSet::new(),
|
soloed: std::collections::HashSet::new(),
|
||||||
|
last_tempo: 0.0,
|
||||||
|
last_beat: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +710,8 @@ impl SequencerState {
|
|||||||
|
|
||||||
pub fn tick(&mut self, input: TickInput) -> TickOutput {
|
pub fn tick(&mut self, input: TickInput) -> TickOutput {
|
||||||
self.process_commands(input.commands);
|
self.process_commands(input.commands);
|
||||||
|
self.last_tempo = input.tempo;
|
||||||
|
self.last_beat = input.beat;
|
||||||
|
|
||||||
if !input.playing {
|
if !input.playing {
|
||||||
return self.tick_paused();
|
return self.tick_paused();
|
||||||
@@ -1092,6 +1103,8 @@ impl SequencerState {
|
|||||||
.collect(),
|
.collect(),
|
||||||
step_traces: Arc::clone(&self.step_traces),
|
step_traces: Arc::clone(&self.step_traces),
|
||||||
event_count: self.event_count,
|
event_count: self.event_count,
|
||||||
|
tempo: self.last_tempo,
|
||||||
|
beat: self.last_beat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
|||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
@@ -247,7 +247,7 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
|||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
|||||||
selected: false,
|
selected: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
@@ -103,9 +103,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
|||||||
}
|
}
|
||||||
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
|
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
|
||||||
KeyCode::Char('l') => super::open_load(ctx),
|
KeyCode::Char('l') => super::open_load(ctx),
|
||||||
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
|
KeyCode::Char('+') | KeyCode::Char('=') if !ctx.app.plugin_mode => {
|
||||||
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
|
ctx.dispatch(AppCommand::TempoUp);
|
||||||
KeyCode::Char('T') => {
|
}
|
||||||
|
KeyCode::Char('-') if !ctx.app.plugin_mode => ctx.dispatch(AppCommand::TempoDown),
|
||||||
|
KeyCode::Char('T') if !ctx.app.plugin_mode => {
|
||||||
let current = format!("{:.1}", ctx.link.tempo());
|
let current = format!("{:.1}", ctx.link.tempo());
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect)
|
|||||||
let focus = ctx.app.options.focus;
|
let focus = ctx.app.options.focus;
|
||||||
let plugin_mode = ctx.app.plugin_mode;
|
let plugin_mode = ctx.app.plugin_mode;
|
||||||
let focus_line = focus.line_index(plugin_mode);
|
let focus_line = focus.line_index(plugin_mode);
|
||||||
let total_lines = if plugin_mode { 43 } else { 40 };
|
let total_lines = crate::state::options::total_lines(plugin_mode);
|
||||||
let max_visible = padded.height as usize;
|
let max_visible = padded.height as usize;
|
||||||
|
|
||||||
let scroll_offset = if total_lines <= max_visible {
|
let scroll_offset = if total_lines <= max_visible {
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> Inpu
|
|||||||
KeyCode::Left | KeyCode::Right => {
|
KeyCode::Left | KeyCode::Right => {
|
||||||
cycle_option_value(ctx, key.code == KeyCode::Right);
|
cycle_option_value(ctx, key.code == KeyCode::Right);
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') if !ctx.app.plugin_mode => {
|
||||||
ctx.dispatch(AppCommand::TogglePlaying);
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
ctx.playing
|
ctx.playing
|
||||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -56,9 +56,31 @@ impl CyclicEnum for OptionsFocus {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line indices when Font/ZoomFactor are shown (plugin mode).
|
const PLUGIN_ONLY: &[OptionsFocus] = &[
|
||||||
// In terminal mode, Font/ZoomFactor are absent; all lines after ShowPreview shift up by 2.
|
OptionsFocus::Font,
|
||||||
const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
OptionsFocus::ZoomFactor,
|
||||||
|
OptionsFocus::WindowSize,
|
||||||
|
];
|
||||||
|
|
||||||
|
const STANDALONE_ONLY: &[OptionsFocus] = &[
|
||||||
|
OptionsFocus::LinkEnabled,
|
||||||
|
OptionsFocus::StartStopSync,
|
||||||
|
OptionsFocus::Quantum,
|
||||||
|
OptionsFocus::MidiOutput0,
|
||||||
|
OptionsFocus::MidiOutput1,
|
||||||
|
OptionsFocus::MidiOutput2,
|
||||||
|
OptionsFocus::MidiOutput3,
|
||||||
|
OptionsFocus::MidiInput0,
|
||||||
|
OptionsFocus::MidiInput1,
|
||||||
|
OptionsFocus::MidiInput2,
|
||||||
|
OptionsFocus::MidiInput3,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Section layout: header line, divider line, then option lines.
|
||||||
|
/// Each entry gives the raw line offsets assuming ALL sections are visible
|
||||||
|
/// (plugin mode with Font/Zoom/Window shown).
|
||||||
|
const FULL_LAYOUT: &[(OptionsFocus, usize)] = &[
|
||||||
|
// DISPLAY section: header=0, divider=1
|
||||||
(OptionsFocus::ColorScheme, 2),
|
(OptionsFocus::ColorScheme, 2),
|
||||||
(OptionsFocus::HueRotation, 3),
|
(OptionsFocus::HueRotation, 3),
|
||||||
(OptionsFocus::RefreshRate, 4),
|
(OptionsFocus::RefreshRate, 4),
|
||||||
@@ -70,51 +92,112 @@ const FOCUS_LINES: &[(OptionsFocus, usize)] = &[
|
|||||||
(OptionsFocus::Font, 10),
|
(OptionsFocus::Font, 10),
|
||||||
(OptionsFocus::ZoomFactor, 11),
|
(OptionsFocus::ZoomFactor, 11),
|
||||||
(OptionsFocus::WindowSize, 12),
|
(OptionsFocus::WindowSize, 12),
|
||||||
|
// blank=13, ABLETON LINK header=14, divider=15
|
||||||
(OptionsFocus::LinkEnabled, 16),
|
(OptionsFocus::LinkEnabled, 16),
|
||||||
(OptionsFocus::StartStopSync, 17),
|
(OptionsFocus::StartStopSync, 17),
|
||||||
(OptionsFocus::Quantum, 18),
|
(OptionsFocus::Quantum, 18),
|
||||||
|
// blank=19, SESSION header=20, divider=21, Tempo=22, Beat=23, Phase=24
|
||||||
|
// blank=25, MIDI OUTPUTS header=26, divider=27
|
||||||
(OptionsFocus::MidiOutput0, 28),
|
(OptionsFocus::MidiOutput0, 28),
|
||||||
(OptionsFocus::MidiOutput1, 29),
|
(OptionsFocus::MidiOutput1, 29),
|
||||||
(OptionsFocus::MidiOutput2, 30),
|
(OptionsFocus::MidiOutput2, 30),
|
||||||
(OptionsFocus::MidiOutput3, 31),
|
(OptionsFocus::MidiOutput3, 31),
|
||||||
|
// blank=32, MIDI INPUTS header=33, divider=34
|
||||||
(OptionsFocus::MidiInput0, 35),
|
(OptionsFocus::MidiInput0, 35),
|
||||||
(OptionsFocus::MidiInput1, 36),
|
(OptionsFocus::MidiInput1, 36),
|
||||||
(OptionsFocus::MidiInput2, 37),
|
(OptionsFocus::MidiInput2, 37),
|
||||||
(OptionsFocus::MidiInput3, 38),
|
(OptionsFocus::MidiInput3, 38),
|
||||||
|
// blank=39, ONBOARDING header=40, divider=41
|
||||||
(OptionsFocus::ResetOnboarding, 42),
|
(OptionsFocus::ResetOnboarding, 42),
|
||||||
];
|
];
|
||||||
|
|
||||||
const PLUGIN_ONLY: &[OptionsFocus] = &[OptionsFocus::Font, OptionsFocus::ZoomFactor, OptionsFocus::WindowSize];
|
|
||||||
|
|
||||||
impl OptionsFocus {
|
impl OptionsFocus {
|
||||||
fn is_plugin_only(self) -> bool {
|
fn is_plugin_only(self) -> bool {
|
||||||
PLUGIN_ONLY.contains(&self)
|
PLUGIN_ONLY.contains(&self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_standalone_only(self) -> bool {
|
||||||
|
STANDALONE_ONLY.contains(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_visible(self, plugin_mode: bool) -> bool {
|
||||||
|
if self.is_plugin_only() && !plugin_mode {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.is_standalone_only() && plugin_mode {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn line_index(self, plugin_mode: bool) -> usize {
|
pub fn line_index(self, plugin_mode: bool) -> usize {
|
||||||
let base = FOCUS_LINES
|
visible_layout(plugin_mode)
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(f, _)| *f == self)
|
.find(|(f, _)| *f == self)
|
||||||
.map(|(_, l)| *l)
|
.map(|(_, l)| *l)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0)
|
||||||
if plugin_mode || base <= 9 {
|
|
||||||
base
|
|
||||||
} else {
|
|
||||||
base - 3
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> {
|
pub fn at_line(line: usize, plugin_mode: bool) -> Option<OptionsFocus> {
|
||||||
FOCUS_LINES.iter().find_map(|(f, l)| {
|
visible_layout(plugin_mode)
|
||||||
if f.is_plugin_only() && !plugin_mode {
|
.iter()
|
||||||
return None;
|
.find(|(_, l)| *l == line)
|
||||||
}
|
.map(|(f, _)| *f)
|
||||||
let effective = if plugin_mode || *l <= 9 { *l } else { *l - 3 };
|
|
||||||
if effective == line { Some(*f) } else { None }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Total number of rendered lines for the options view.
|
||||||
|
pub fn total_lines(plugin_mode: bool) -> usize {
|
||||||
|
visible_layout(plugin_mode)
|
||||||
|
.last()
|
||||||
|
.map(|(_, l)| *l + 1)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute (focus, line_index) pairs for only the visible options,
|
||||||
|
/// with line indices adjusted to account for hidden sections.
|
||||||
|
fn visible_layout(plugin_mode: bool) -> Vec<(OptionsFocus, usize)> {
|
||||||
|
// Start from the full layout and compute adjusted line numbers.
|
||||||
|
// Hidden items + their section headers/dividers/blanks shrink the layout.
|
||||||
|
|
||||||
|
// We know the exact section structure, so we compute the offset to subtract
|
||||||
|
// based on which sections are hidden.
|
||||||
|
let mut offset: usize = 0;
|
||||||
|
|
||||||
|
// Font/Zoom/Window lines (10,11,12) hidden when !plugin_mode
|
||||||
|
if !plugin_mode {
|
||||||
|
offset += 3; // 3 lines for Font, ZoomFactor, WindowSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link + Session + MIDI sections hidden when plugin_mode
|
||||||
|
// These span from blank(13) through MidiInput3(38) = 26 lines
|
||||||
|
if plugin_mode {
|
||||||
|
// blank + LINK header + divider + 3 options + blank + SESSION header + divider + 3 readonlys
|
||||||
|
// + blank + MIDI OUT header + divider + 4 options + blank + MIDI IN header + divider + 4 options
|
||||||
|
// = 26 lines (indices 13..=38)
|
||||||
|
let link_section_lines = 26;
|
||||||
|
offset += link_section_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for &(focus, raw_line) in FULL_LAYOUT {
|
||||||
|
if !focus.is_visible(plugin_mode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Lines at or below index 9 (ShowPreview) are never shifted
|
||||||
|
let adjusted = if raw_line <= 9 {
|
||||||
|
raw_line
|
||||||
|
} else if !plugin_mode && raw_line <= 12 {
|
||||||
|
// Font/Zoom/Window — these are hidden, skip
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
raw_line - offset
|
||||||
|
};
|
||||||
|
result.push((focus, adjusted));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct OptionsState {
|
pub struct OptionsState {
|
||||||
pub focus: OptionsFocus,
|
pub focus: OptionsFocus,
|
||||||
@@ -125,7 +208,7 @@ impl OptionsState {
|
|||||||
let mut f = self.focus;
|
let mut f = self.focus;
|
||||||
loop {
|
loop {
|
||||||
f = f.next();
|
f = f.next();
|
||||||
if !f.is_plugin_only() || plugin_mode {
|
if f.is_visible(plugin_mode) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +219,7 @@ impl OptionsState {
|
|||||||
let mut f = self.focus;
|
let mut f = self.focus;
|
||||||
loop {
|
loop {
|
||||||
f = f.prev();
|
f = f.prev();
|
||||||
if !f.is_plugin_only() || plugin_mode {
|
if f.is_visible(plugin_mode) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
// Page-specific bindings
|
// Page-specific bindings
|
||||||
match page {
|
match page {
|
||||||
Page::Main => {
|
Page::Main => {
|
||||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
if !plugin_mode {
|
||||||
|
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||||
|
}
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
||||||
bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
|
bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
|
||||||
bindings.push(("Esc", "Clear", "Clear selection"));
|
bindings.push(("Esc", "Clear", "Clear selection"));
|
||||||
@@ -33,8 +35,10 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
bindings.push(("Del", "Delete", "Delete step(s)"));
|
bindings.push(("Del", "Delete", "Delete step(s)"));
|
||||||
bindings.push(("< >", "Length", "Decrease/increase pattern length"));
|
bindings.push(("< >", "Length", "Decrease/increase pattern length"));
|
||||||
bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed"));
|
bindings.push(("[ ]", "Speed", "Decrease/increase pattern speed"));
|
||||||
bindings.push(("+ -", "Tempo", "Increase/decrease tempo"));
|
if !plugin_mode {
|
||||||
bindings.push(("T", "Set tempo", "Open tempo input"));
|
bindings.push(("+ -", "Tempo", "Increase/decrease tempo"));
|
||||||
|
bindings.push(("T", "Set tempo", "Open tempo input"));
|
||||||
|
}
|
||||||
bindings.push(("L", "Set length", "Open length input"));
|
bindings.push(("L", "Set length", "Open length input"));
|
||||||
bindings.push(("S", "Set speed", "Open speed input"));
|
bindings.push(("S", "Set speed", "Open speed input"));
|
||||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||||
@@ -52,7 +56,9 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
Page::Patterns => {
|
Page::Patterns => {
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||||
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
if !plugin_mode {
|
||||||
|
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||||
|
}
|
||||||
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
||||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||||
@@ -72,20 +78,26 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
bindings.push(("↑↓", "Navigate", "Navigate list items"));
|
bindings.push(("↑↓", "Navigate", "Navigate list items"));
|
||||||
bindings.push(("PgUp/Dn", "Page", "Page through device list"));
|
bindings.push(("PgUp/Dn", "Page", "Page through device list"));
|
||||||
bindings.push(("Enter", "Select", "Select device"));
|
bindings.push(("Enter", "Select", "Select device"));
|
||||||
bindings.push(("R", "Restart", "Restart audio engine"));
|
if !plugin_mode {
|
||||||
|
bindings.push(("R", "Restart", "Restart audio engine"));
|
||||||
|
}
|
||||||
bindings.push(("A", "Add path", "Add sample path"));
|
bindings.push(("A", "Add path", "Add sample path"));
|
||||||
bindings.push(("D", "Refresh/Del", "Refresh devices or delete path"));
|
bindings.push(("D", "Refresh/Del", "Refresh devices or delete path"));
|
||||||
bindings.push(("h", "Hush", "Stop all sounds gracefully"));
|
bindings.push(("h", "Hush", "Stop all sounds gracefully"));
|
||||||
bindings.push(("p", "Panic", "Stop all sounds immediately"));
|
bindings.push(("p", "Panic", "Stop all sounds immediately"));
|
||||||
bindings.push(("r", "Reset", "Reset peak voice counter"));
|
bindings.push(("r", "Reset", "Reset peak voice counter"));
|
||||||
bindings.push(("t", "Test", "Play test tone"));
|
if !plugin_mode {
|
||||||
|
bindings.push(("t", "Test", "Play test tone"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Page::Options => {
|
Page::Options => {
|
||||||
bindings.push(("Tab", "Next", "Move to next option"));
|
bindings.push(("Tab", "Next", "Move to next option"));
|
||||||
bindings.push(("Shift+Tab", "Previous", "Move to previous option"));
|
bindings.push(("Shift+Tab", "Previous", "Move to previous option"));
|
||||||
bindings.push(("↑↓", "Navigate", "Navigate options"));
|
bindings.push(("↑↓", "Navigate", "Navigate options"));
|
||||||
bindings.push(("←→", "Toggle", "Toggle or adjust option"));
|
bindings.push(("←→", "Toggle", "Toggle or adjust option"));
|
||||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
if !plugin_mode {
|
||||||
|
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Page::Help => {
|
Page::Help => {
|
||||||
bindings.push(("↑↓ j/k", "Scroll", "Scroll content"));
|
bindings.push(("↑↓ j/k", "Scroll", "Scroll content"));
|
||||||
|
|||||||
@@ -32,84 +32,6 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
let focus = app.options.focus;
|
let focus = app.options.focus;
|
||||||
let content_width = padded.width as usize;
|
let content_width = padded.width as usize;
|
||||||
|
|
||||||
let enabled = link.is_enabled();
|
|
||||||
let peers = link.peers();
|
|
||||||
let (status_text, status_color) = if !enabled {
|
|
||||||
("DISABLED", theme.link_status.disabled)
|
|
||||||
} else if peers > 0 {
|
|
||||||
("CONNECTED", theme.link_status.connected)
|
|
||||||
} else {
|
|
||||||
("LISTENING", theme.link_status.listening)
|
|
||||||
};
|
|
||||||
let peer_text = if enabled && peers > 0 {
|
|
||||||
if peers == 1 {
|
|
||||||
" · 1 peer".to_string()
|
|
||||||
} else {
|
|
||||||
format!(" · {peers} peers")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let link_header = Line::from(vec![
|
|
||||||
Span::styled(
|
|
||||||
"ABLETON LINK",
|
|
||||||
Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(
|
|
||||||
status_text,
|
|
||||||
Style::new().fg(status_color).add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let quantum_str = format!("{:.0}", link.quantum());
|
|
||||||
let tempo_str = format!("{:.1} BPM", link.tempo());
|
|
||||||
let beat_str = format!("{:.2}", link.beat());
|
|
||||||
let phase_str = format!("{:.2}", link.phase());
|
|
||||||
|
|
||||||
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
|
|
||||||
let value_style = Style::new().fg(theme.values.value);
|
|
||||||
|
|
||||||
let midi_outputs = midi::list_midi_outputs();
|
|
||||||
let midi_inputs = midi::list_midi_inputs();
|
|
||||||
|
|
||||||
let midi_out_display = |slot: usize| -> String {
|
|
||||||
if let Some(idx) = app.midi.selected_outputs[slot] {
|
|
||||||
midi_outputs
|
|
||||||
.get(idx)
|
|
||||||
.map(|d| d.name.clone())
|
|
||||||
.unwrap_or_else(|| "(disconnected)".to_string())
|
|
||||||
} else if midi_outputs.is_empty() {
|
|
||||||
"(none found)".to_string()
|
|
||||||
} else {
|
|
||||||
"(not connected)".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let midi_in_display = |slot: usize| -> String {
|
|
||||||
if let Some(idx) = app.midi.selected_inputs[slot] {
|
|
||||||
midi_inputs
|
|
||||||
.get(idx)
|
|
||||||
.map(|d| d.name.clone())
|
|
||||||
.unwrap_or_else(|| "(disconnected)".to_string())
|
|
||||||
} else if midi_inputs.is_empty() {
|
|
||||||
"(none found)".to_string()
|
|
||||||
} else {
|
|
||||||
"(not connected)".to_string()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let midi_out_0 = midi_out_display(0);
|
|
||||||
let midi_out_1 = midi_out_display(1);
|
|
||||||
let midi_out_2 = midi_out_display(2);
|
|
||||||
let midi_out_3 = midi_out_display(3);
|
|
||||||
let midi_in_0 = midi_in_display(0);
|
|
||||||
let midi_in_1 = midi_in_display(1);
|
|
||||||
let midi_in_2 = midi_in_display(2);
|
|
||||||
let midi_in_3 = midi_in_display(3);
|
|
||||||
|
|
||||||
let onboarding_str = format!("{}/6 dismissed", app.ui.onboarding_dismissed.len());
|
let onboarding_str = format!("{}/6 dismissed", app.ui.onboarding_dismissed.len());
|
||||||
let hue_str = format!("{}°", app.ui.hue_rotation as i32);
|
let hue_str = format!("{}°", app.ui.hue_rotation as i32);
|
||||||
|
|
||||||
@@ -169,9 +91,9 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
&theme,
|
&theme,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
|
|
||||||
let window_str = format!("{}x{}", app.ui.window_width, app.ui.window_height);
|
|
||||||
if app.plugin_mode {
|
if app.plugin_mode {
|
||||||
|
let zoom_str = format!("{:.0}%", app.ui.zoom_factor * 100.0);
|
||||||
|
let window_str = format!("{}x{}", app.ui.window_width, app.ui.window_height);
|
||||||
lines.push(render_option_line(
|
lines.push(render_option_line(
|
||||||
"Font",
|
"Font",
|
||||||
&app.ui.font,
|
&app.ui.font,
|
||||||
@@ -191,48 +113,127 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
&theme,
|
&theme,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if !app.plugin_mode {
|
||||||
|
let enabled = link.is_enabled();
|
||||||
|
let peers = link.peers();
|
||||||
|
let (status_text, status_color) = if !enabled {
|
||||||
|
("DISABLED", theme.link_status.disabled)
|
||||||
|
} else if peers > 0 {
|
||||||
|
("CONNECTED", theme.link_status.connected)
|
||||||
|
} else {
|
||||||
|
("LISTENING", theme.link_status.listening)
|
||||||
|
};
|
||||||
|
let peer_text = if enabled && peers > 0 {
|
||||||
|
if peers == 1 {
|
||||||
|
" · 1 peer".to_string()
|
||||||
|
} else {
|
||||||
|
format!(" · {peers} peers")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let link_header = Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
"ABLETON LINK",
|
||||||
|
Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(
|
||||||
|
status_text,
|
||||||
|
Style::new().fg(status_color).add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)),
|
||||||
|
]);
|
||||||
|
let quantum_str = format!("{:.0}", link.quantum());
|
||||||
|
let tempo_str = format!("{:.1} BPM", link.tempo());
|
||||||
|
let beat_str = format!("{:.2}", link.beat());
|
||||||
|
let phase_str = format!("{:.2}", link.phase());
|
||||||
|
let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD);
|
||||||
|
let value_style = Style::new().fg(theme.values.value);
|
||||||
|
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.extend([
|
||||||
|
link_header,
|
||||||
|
render_divider(content_width, &theme),
|
||||||
|
render_option_line(
|
||||||
|
"Enabled",
|
||||||
|
if link.is_enabled() { "On" } else { "Off" },
|
||||||
|
focus == OptionsFocus::LinkEnabled,
|
||||||
|
&theme,
|
||||||
|
),
|
||||||
|
render_option_line(
|
||||||
|
"Start/Stop sync",
|
||||||
|
if link.is_start_stop_sync_enabled() {
|
||||||
|
"On"
|
||||||
|
} else {
|
||||||
|
"Off"
|
||||||
|
},
|
||||||
|
focus == OptionsFocus::StartStopSync,
|
||||||
|
&theme,
|
||||||
|
),
|
||||||
|
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
|
||||||
|
Line::from(""),
|
||||||
|
render_section_header("SESSION", &theme),
|
||||||
|
render_divider(content_width, &theme),
|
||||||
|
render_readonly_line("Tempo", &tempo_str, tempo_style, &theme),
|
||||||
|
render_readonly_line("Beat", &beat_str, value_style, &theme),
|
||||||
|
render_readonly_line("Phase", &phase_str, value_style, &theme),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let midi_outputs = midi::list_midi_outputs();
|
||||||
|
let midi_inputs = midi::list_midi_inputs();
|
||||||
|
let midi_out_display = |slot: usize| -> String {
|
||||||
|
if let Some(idx) = app.midi.selected_outputs[slot] {
|
||||||
|
midi_outputs
|
||||||
|
.get(idx)
|
||||||
|
.map(|d| d.name.clone())
|
||||||
|
.unwrap_or_else(|| "(disconnected)".to_string())
|
||||||
|
} else if midi_outputs.is_empty() {
|
||||||
|
"(none found)".to_string()
|
||||||
|
} else {
|
||||||
|
"(not connected)".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let midi_in_display = |slot: usize| -> String {
|
||||||
|
if let Some(idx) = app.midi.selected_inputs[slot] {
|
||||||
|
midi_inputs
|
||||||
|
.get(idx)
|
||||||
|
.map(|d| d.name.clone())
|
||||||
|
.unwrap_or_else(|| "(disconnected)".to_string())
|
||||||
|
} else if midi_inputs.is_empty() {
|
||||||
|
"(none found)".to_string()
|
||||||
|
} else {
|
||||||
|
"(not connected)".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let midi_out_0 = midi_out_display(0);
|
||||||
|
let midi_out_1 = midi_out_display(1);
|
||||||
|
let midi_out_2 = midi_out_display(2);
|
||||||
|
let midi_out_3 = midi_out_display(3);
|
||||||
|
let midi_in_0 = midi_in_display(0);
|
||||||
|
let midi_in_1 = midi_in_display(1);
|
||||||
|
let midi_in_2 = midi_in_display(2);
|
||||||
|
let midi_in_3 = midi_in_display(3);
|
||||||
|
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.extend([
|
||||||
|
render_section_header("MIDI OUTPUTS", &theme),
|
||||||
|
render_divider(content_width, &theme),
|
||||||
|
render_option_line("Output 0", &midi_out_0, focus == OptionsFocus::MidiOutput0, &theme),
|
||||||
|
render_option_line("Output 1", &midi_out_1, focus == OptionsFocus::MidiOutput1, &theme),
|
||||||
|
render_option_line("Output 2", &midi_out_2, focus == OptionsFocus::MidiOutput2, &theme),
|
||||||
|
render_option_line("Output 3", &midi_out_3, focus == OptionsFocus::MidiOutput3, &theme),
|
||||||
|
Line::from(""),
|
||||||
|
render_section_header("MIDI INPUTS", &theme),
|
||||||
|
render_divider(content_width, &theme),
|
||||||
|
render_option_line("Input 0", &midi_in_0, focus == OptionsFocus::MidiInput0, &theme),
|
||||||
|
render_option_line("Input 1", &midi_in_1, focus == OptionsFocus::MidiInput1, &theme),
|
||||||
|
render_option_line("Input 2", &midi_in_2, focus == OptionsFocus::MidiInput2, &theme),
|
||||||
|
render_option_line("Input 3", &midi_in_3, focus == OptionsFocus::MidiInput3, &theme),
|
||||||
|
]);
|
||||||
|
}
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
lines.extend([
|
lines.extend([
|
||||||
link_header,
|
|
||||||
render_divider(content_width, &theme),
|
|
||||||
render_option_line(
|
|
||||||
"Enabled",
|
|
||||||
if link.is_enabled() { "On" } else { "Off" },
|
|
||||||
focus == OptionsFocus::LinkEnabled,
|
|
||||||
&theme,
|
|
||||||
),
|
|
||||||
render_option_line(
|
|
||||||
"Start/Stop sync",
|
|
||||||
if link.is_start_stop_sync_enabled() {
|
|
||||||
"On"
|
|
||||||
} else {
|
|
||||||
"Off"
|
|
||||||
},
|
|
||||||
focus == OptionsFocus::StartStopSync,
|
|
||||||
&theme,
|
|
||||||
),
|
|
||||||
render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme),
|
|
||||||
Line::from(""),
|
|
||||||
render_section_header("SESSION", &theme),
|
|
||||||
render_divider(content_width, &theme),
|
|
||||||
render_readonly_line("Tempo", &tempo_str, tempo_style, &theme),
|
|
||||||
render_readonly_line("Beat", &beat_str, value_style, &theme),
|
|
||||||
render_readonly_line("Phase", &phase_str, value_style, &theme),
|
|
||||||
Line::from(""),
|
|
||||||
render_section_header("MIDI OUTPUTS", &theme),
|
|
||||||
render_divider(content_width, &theme),
|
|
||||||
render_option_line("Output 0", &midi_out_0, focus == OptionsFocus::MidiOutput0, &theme),
|
|
||||||
render_option_line("Output 1", &midi_out_1, focus == OptionsFocus::MidiOutput1, &theme),
|
|
||||||
render_option_line("Output 2", &midi_out_2, focus == OptionsFocus::MidiOutput2, &theme),
|
|
||||||
render_option_line("Output 3", &midi_out_3, focus == OptionsFocus::MidiOutput3, &theme),
|
|
||||||
Line::from(""),
|
|
||||||
render_section_header("MIDI INPUTS", &theme),
|
|
||||||
render_divider(content_width, &theme),
|
|
||||||
render_option_line("Input 0", &midi_in_0, focus == OptionsFocus::MidiInput0, &theme),
|
|
||||||
render_option_line("Input 1", &midi_in_1, focus == OptionsFocus::MidiInput1, &theme),
|
|
||||||
render_option_line("Input 2", &midi_in_2, focus == OptionsFocus::MidiInput2, &theme),
|
|
||||||
render_option_line("Input 3", &midi_in_3, focus == OptionsFocus::MidiInput3, &theme),
|
|
||||||
Line::from(""),
|
|
||||||
render_section_header("ONBOARDING", &theme),
|
render_section_header("ONBOARDING", &theme),
|
||||||
render_divider(content_width, &theme),
|
render_divider(content_width, &theme),
|
||||||
render_option_line(
|
render_option_line(
|
||||||
@@ -290,7 +291,7 @@ fn render_divider(width: usize, theme: &theme::ThemeColors) -> Line<'static> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme: &theme::ThemeColors) -> Line<'a> {
|
fn render_option_line(label: &str, value: &str, focused: bool, theme: &theme::ThemeColors) -> Line<'static> {
|
||||||
let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD);
|
let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD);
|
||||||
let normal = Style::new().fg(theme.ui.text_primary);
|
let normal = Style::new().fg(theme.ui.text_primary);
|
||||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||||
@@ -309,19 +310,19 @@ fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme:
|
|||||||
let padded_label = format!("{label:<label_width$}");
|
let padded_label = format!("{label:<label_width$}");
|
||||||
|
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(prefix, prefix_style),
|
Span::styled(prefix.to_string(), prefix_style),
|
||||||
Span::styled(padded_label, label_style),
|
Span::styled(padded_label, label_style),
|
||||||
Span::styled(value_display, value_style),
|
Span::styled(value_display, value_style),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style, theme: &theme::ThemeColors) -> Line<'a> {
|
fn render_readonly_line(label: &str, value: &str, value_style: Style, theme: &theme::ThemeColors) -> Line<'static> {
|
||||||
let label_style = Style::new().fg(theme.ui.text_muted);
|
let label_style = Style::new().fg(theme.ui.text_muted);
|
||||||
let label_width = 20;
|
let label_width = 20;
|
||||||
let padded_label = format!("{label:<label_width$}");
|
let padded_label = format!("{label:<label_width$}");
|
||||||
|
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::styled(" ".to_string(), Style::new()),
|
||||||
Span::styled(padded_label, label_style),
|
Span::styled(padded_label, label_style),
|
||||||
Span::styled(format!(" {value}"), value_style),
|
Span::styled(format!(" {value}"), value_style),
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user