diff --git a/src/cli.rs b/src/cli.rs index 03719e55..52d2702c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,12 +11,17 @@ pub struct Cli { pub enum Command { /// Launch or control a master transport Transport, + /// Launch or control a sequencer + Sequencer { + #[arg(long="input")] + inputs: Vec>, + #[arg(long="output")] + outputs: Vec>, + }, + /// Launch or control a sampler + Sampler, /// Launch or control a mixer Mixer, /// Launch or control a looper Looper, - /// Launch or control a sampler - Sampler, - /// Launch or control a sequencer - Sequencer, } diff --git a/src/device/launcher.rs b/src/device/launcher.rs new file mode 100644 index 00000000..f0fd78b4 --- /dev/null +++ b/src/device/launcher.rs @@ -0,0 +1,37 @@ + let mut x = areas[1].x; + for (index, track) in [ + "Track 1", + "Track 2", + "Track 3", + "Track 4", + "Track 5", + "Bus 1", + "Bus 2", + "Mix", + ].iter().enumerate() { + buffer.set_string( + x + 10 * (index + 1) as u16, areas[1].y, + "┬", Style::default().not_bold().dim() + ); + buffer.set_string( + x + 10 * (index + 1) as u16, areas[1].y + areas[1].height - 1, + "┴", Style::default().not_bold().dim() + ); + for y in areas[1].y+1..areas[1].y+areas[1].height - 1 { + buffer.set_string( + x + 10 * (index + 1) as u16, y, + "│", Style::default().not_bold().gray().dim() + ); + } + for y in areas[1].y+2..areas[1].y+areas[1].height - 1 { + buffer.set_string( + x + 10 * index as u16 + 1, y, + "--------", Style::default().not_bold().gray().dim() + ); + } + buffer.set_string( + x + 10 * index as u16 + 1, areas[1].y + 1, + track, Style::default().bold().not_dim() + ); + } + diff --git a/src/device/lv2_host.rs b/src/device/lv2_host.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/device/mixer.rs b/src/device/mixer.rs index f0d074e5..4b2f4f56 100644 --- a/src/device/mixer.rs +++ b/src/device/mixer.rs @@ -55,10 +55,11 @@ impl Mixer { selected_column: 0, selected_track: 1, tracks: vec![ - Track::new(&jack.as_client(), 1, "Kick")?, - Track::new(&jack.as_client(), 1, "Snare")?, - Track::new(&jack.as_client(), 2, "Hihats")?, - Track::new(&jack.as_client(), 2, "Sample")?, + Track::new(&jack.as_client(), 1, "Mono 1")?, + Track::new(&jack.as_client(), 1, "Mono 2")?, + Track::new(&jack.as_client(), 2, "Stereo 1")?, + Track::new(&jack.as_client(), 2, "Stereo 2")?, + Track::new(&jack.as_client(), 2, "Stereo 3")?, Track::new(&jack.as_client(), 2, "Bus 1")?, Track::new(&jack.as_client(), 2, "Bus 2")?, Track::new(&jack.as_client(), 2, "Mix")?, @@ -114,68 +115,62 @@ impl Exitable for Mixer { impl WidgetRef for Mixer { fn render_ref (&self, area: Rect, buf: &mut Buffer) { - } -} - -pub fn render ( - state: &mut Mixer, - stdout: &mut Stdout, - mut offset: (u16, u16) -) -> Result<(), Box> { - render_table(state, stdout, offset)?; - render_meters(state, stdout, offset)?; - Ok(()) -} - -fn render_table ( - state: &mut Mixer, - stdout: &mut Stdout, - offset: (u16, u16) -) -> Result<(), Box> { - let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - stdout.queue( - move_to(0, 0) - )?.queue( - Print(" Name Gain FX1 Pan Level FX2 Route") - )?; - for (i, track) in state.tracks.iter().enumerate() { - let row = (i + 1) as u16; - for (j, (column, field)) in [ - (0, format!(" {:7} ", track.name)), - (12, format!(" {:.1}dB ", track.gain)), - (22, format!(" [ ] ")), - (30, format!(" C ")), - (35, format!(" {:.1}dB ", track.level)), - (45, format!(" [ ] ")), - (51, format!(" {:7} ", track.route)), - ].into_iter().enumerate() { - stdout.queue(move_to(column, row))?; - if state.selected_track == i && state.selected_column == j { - stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; - } else { - stdout.queue(PrintStyledContent(field.to_string().bold()))?; + use ratatui::style::Stylize; + draw_box(buf, area); + let x = area.x + 1; + let y = area.y + 1; + let h = area.height - 2; + for (i, track) in self.tracks.iter().enumerate() { + //buf.set_string( + //x, y + index as u16, + //&track.name, Style::default().bold().not_dim() + //); + for (j, (column, field)) in [ + (0, format!(" {:7} ", track.name)), + (12, format!(" {:.1}dB ", track.gain)), + (22, format!(" [ ] ")), + (30, format!(" C ")), + (35, format!(" {:.1}dB ", track.level)), + (45, format!(" [ ] ")), + (51, format!(" {:7} ", track.route)), + ].into_iter().enumerate() { + buf.set_string( + x + column as u16, + y + i as u16, + field, + if self.selected_track == i && self.selected_column == j { + Style::default().white().bold().not_dim() + } else { + Style::default().not_dim() + } + ); + //stdout.queue(move_to(column, row))?; + //if state.selected_track == i && state.selected_column == j { + //stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; + //} else { + //stdout.queue(PrintStyledContent(field.to_string().bold()))?; + //} + //fn render_meters ( + //state: &mut Mixer, + //stdout: &mut Stdout, + //offset: (u16, u16) + //) -> Result<(), Box> { + //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); + //for (i, track) in state.tracks.iter().enumerate() { + //let row = (i + 1) as u16; + //stdout + //.queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?; + //} + //Ok(()) + //} } } } - Ok(()) } -fn render_meters ( - state: &mut Mixer, - stdout: &mut Stdout, - offset: (u16, u16) -) -> Result<(), Box> { - let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - for (i, track) in state.tracks.iter().enumerate() { - let row = (i + 1) as u16; - stdout - .queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))? - .queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))? - .queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))? - .queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?; - } - Ok(()) -} impl HandleInput for Mixer { fn handle (&mut self, event: &Event) -> Result<(), Box> { diff --git a/src/device/port_list.rs b/src/device/port_list.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/device/sequencer.rs b/src/device/sequencer.rs index 6f7a9d73..1315b267 100644 --- a/src/device/sequencer.rs +++ b/src/device/sequencer.rs @@ -11,6 +11,9 @@ pub const ACTIONS: [(&'static str, &'static str);4] = [ pub struct Sequencer { name: Arc, exited: Arc, + playing: Arc, + recording: Arc, + overdub: Arc, sequence: Arc>>>>, cursor: (u16, u16, u16), timesig: (f32, f32), @@ -25,12 +28,11 @@ pub enum Event { impl Sequencer { - pub fn new (name: Option<&str>) -> Result> { - let exited = Arc::new(AtomicBool::new(false)); - let name = name.unwrap_or("sequencer"); - let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; - let mut port = client.register_port("sequence", ::jack::MidiOut::default())?; - let sequence: Arc>>>> = Arc::new(Mutex::new(vec![vec![None;64];128])); + pub fn new ( + name: Option<&str>, + connect_inputs: Option<&[String]>, + connect_outputs: Option<&[String]>, + ) -> Result> { let beats = 4; let steps = 16; let bpm = 120.0; @@ -40,6 +42,23 @@ impl Sequencer { let t_beat = 60.0 / bpm; // msec let t_loop = t_beat * beats as f64; // msec let t_step = t_beat / steps as f64; // msec + let exited = Arc::new(AtomicBool::new(false)); + let playing = Arc::new(AtomicBool::new(true)); + let recording = Arc::new(AtomicBool::new(true)); + let overdub = Arc::new(AtomicBool::new(false)); + let name = name.unwrap_or("sequencer"); + let (client, _status) = Client::new( + name, ClientOptions::NO_START_SERVER + )?; + let mut input = client.register_port( + "input", ::jack::MidiIn::default() + )?; + let mut output = client.register_port( + "output", ::jack::MidiOut::default() + )?; + let sequence: Arc>>>> = Arc::new( + Mutex::new(vec![vec![None;64];128]) + ); let mut step_frames = vec![]; for step in 0..beats*steps { let step_index = (step as f64 * t_step / frame) as usize; @@ -51,11 +70,14 @@ impl Sequencer { frame_steps[*frame] = Some(index); } Ok(Self { - name: name.into(), - exited: exited.clone(), - sequence: sequence.clone(), - cursor: (11, 0, 0), - timesig: (4.0, 4.0), + name: name.into(), + exited: exited.clone(), + playing: playing.clone(), + recording: recording.clone(), + overdub: overdub.clone(), + sequence: sequence.clone(), + cursor: (11, 0, 0), + timesig: (4.0, 4.0), jack_client: crate::engine::activate_jack_client( client, Notifications, @@ -69,28 +91,41 @@ impl Sequencer { let chunk_end = chunk_start + chunk_size; let start_looped = chunk_start as usize % loop_frames; let end_looped = chunk_end as usize % loop_frames; - let mut writer = port.writer(scope); - let sequence = sequence.lock().unwrap(); - for frame in 0..chunk_size { - let value = frame_steps[(start_looped + frame as usize) % loop_frames]; - if let Some(step) = value { - for track in sequence.iter() { - for event in track[step].iter() { - writer.write(&::jack::RawMidi { - time: frame as u32, - bytes: &match event { - Event::NoteOn(pitch, velocity) => [ - 0b10010000, - *pitch, - *velocity - ], - Event::NoteOff(pitch) => [ - 0b10000000, - *pitch, - 0b00000000 - ], - } - }).unwrap() + + // Write MIDI notes from input to sequence + if recording.fetch_and(true, Ordering::Relaxed) { + //let overdub = overdub.fetch_and(true, Ordering::Relaxed); + for event in input.iter(scope) { + let ::jack::RawMidi { time, bytes } = event; + println!("\n{time} {bytes:?}"); + } + } + + // Write MIDI notes from sequence to output + if playing.fetch_and(true, Ordering::Relaxed) { + let mut writer = output.writer(scope); + let sequence = sequence.lock().unwrap(); + for frame in 0..chunk_size { + let value = frame_steps[(start_looped + frame as usize) % loop_frames]; + if let Some(step) = value { + for track in sequence.iter() { + for event in track[step].iter() { + writer.write(&::jack::RawMidi { + time: frame as u32, + bytes: &match event { + Event::NoteOn(pitch, velocity) => [ + 0b10010000, + *pitch, + *velocity + ], + Event::NoteOff(pitch) => [ + 0b10000000, + *pitch, + 0b00000000 + ], + } + }).unwrap() + } } } } @@ -186,44 +221,88 @@ const KEYS: [&'static str; 6] = [ ]; impl WidgetRef for Sequencer { - fn render_ref (&self, area: Rect, buf: &mut Buffer) { - draw_leaf(buf, area, 1, 0, &format!("{} ", &self.name)); - draw_leaf(buf, area, 3, 0, "Channel: 01"); - draw_leaf(buf, area, 5, 0, "Zoom: 1/64"); - draw_leaf(buf, area, 7, 0, "Rate: 1/1"); - - draw_leaf(buf, area, 10, 0, "Inputs: "); - draw_leaf(buf, area, 13, 0, "Outputs: "); - { - let mut area = area.clone(); - area.height = 18; - draw_box(buf, area); - } - { - let mut area = area.inner(&Margin { horizontal: 1, vertical: 3, }); - area.x = area.x + 14; - draw_sequence_keys(area, buf, &self.jack_client.as_client().transport().query().unwrap(), &self.sequence); - draw_sequence_header(area, buf); - draw_sequence_cursor(area, buf, self.cursor); - } + fn render_ref (&self, mut area: Rect, buf: &mut Buffer) { + draw_sequencer(self, buf, area) } } -fn draw_sequence_header ( - area: Rect, buf: &mut Buffer +fn draw_sequencer (sequencer: &Sequencer, buf: &mut Buffer, mut area: Rect) { + area.height = 15; + draw_box(buf, area); + let Rect { x, y, width, height } = area; + let mut command = |y2: u16, c: &str, ommand: &str, value: &str| { + buf.set_string(x + 1, y + y2, c, Style::default().bold()); + buf.set_string(x + 2, y + y2, ommand, Style::default().dim()); + buf.set_string(x + 4 + ommand.len() as u16, y + y2, value, Style::default().bold()); + }; + for (y, c, ommand, value) in [ + (5, "I", "nputs", "[+]"), + (6, "O", "utputs", "[+]"), + (7, "C", "hannel", "01"), + (8, "G", "rid", "1/16"), + (9, "Z", "oom", "1/64"), + (10, "R", "ate", "1/1"), + (11, "S", "ync", "1 bar"), + (12, "A", "dd note", ""), + (13, "D", "elete note", ""), + ] { + command(y, c, ommand, value) + } + { + let mut area = area.clone(); + //area.y = area.y + 1; + area.x = area.x + 19; + area.height = area.height - 2; + draw_sequence_keys(area, buf, + &sequencer.jack_client.as_client().transport().query().unwrap(), + &sequencer.sequence, + &sequencer.cursor); + draw_sequence_cursor(area, buf, + &sequencer.cursor); + } + draw_box(buf, Rect { x, y, width: 18, height }); + draw_rec_dub_button(buf, x, y + 2); + draw_sequence_button(buf, x, y, &sequencer.name); +} + +fn draw_sequence_button ( + buf: &mut Buffer, + x: u16, + y: u16, + name: &str ) { - buf.set_string(area.x + 3, area.y, "╭1.1.", Style::default().dim()); - buf.set_string(area.x + 3 + 16, area.y, "╭1.2.", Style::default().dim()); - buf.set_string(area.x + 3 + 32, area.y, "╭1.3.", Style::default().dim()); - buf.set_string(area.x + 3 + 48, area.y, "╭1.4.", Style::default().dim()); + draw_box(buf, Rect { x, y, width: 18, height: 3 }); + buf.set_string(x + 1, y + 1, &format!(" ▶ {} ", name), + Style::default().white().bold().not_dim()); + buf.set_string(x + 4, y + 0, "┬", Style::default().gray()); + buf.set_string(x + 4, y + 1, "│", Style::default().gray().dim()); + buf.set_string(x + 4, y + 2, "┴", Style::default().gray()); +} + +fn draw_rec_dub_button ( + buf: &mut Buffer, + x: u16, + y: u16, +) { + draw_box(buf, Rect { x, y, width: 18, height: 3 }); + buf.set_string(x + 1, y + 1, " ⏺ REC DUB ", + Style::default().white().bold().not_dim()); + buf.set_string(x + 4, y + 0, "┬", Style::default().gray()); + buf.set_string(x + 4, y + 1, "│", Style::default().gray().dim()); + buf.set_string(x + 4, y + 2, "┴", Style::default().gray()); } fn draw_sequence_keys ( area: Rect, buf: &mut Buffer, transport: &::jack::TransportStatePosition, - sequence: &Arc>>>> + sequence: &Arc>>>>, + cursor: &(u16, u16, u16) ) { + buf.set_string(area.x + 3, area.y, "╭1.1.", Style::default().dim()); + buf.set_string(area.x + 3 + 16, area.y, "╭1.2.", Style::default().dim()); + buf.set_string(area.x + 3 + 32, area.y, "╭1.3.", Style::default().dim()); + buf.set_string(area.x + 3 + 48, area.y, "╭1.4.", Style::default().dim()); //buf.set_string(area.x + 2, area.y, "╭", Style::default().dim()); //buf.set_string(area.x + 2, area.y + 13, "╰", Style::default().dim()); buf.set_string(area.x + 2 + 65, area.y, "╮", Style::default().dim()); @@ -238,17 +317,21 @@ fn draw_sequence_keys ( let bars = beats as u32 / div as u32; let beat = beats as u32 % div as u32 + 1; let beat_sub = beats % 1.0; + //buf.set_string( + //area.x - 18, area.y + area.height, + //format!("BBT {bars:04}:{beat:02}.{:02}", (beat_sub * 16.0) as u32), + //Style::default() + //); let sequence = sequence.lock().unwrap(); - buf.set_string( - area.x + area.width - 25, area.y - 2, format!("{bars:04}:{beat:02}.{:02}", (beat_sub * 16.0) as u32), Style::default() - ); for key in 0..12 { buf.set_string(area.x, area.y + 1 + key, KEYS[(key % 6) as usize], - Style::default().black()); + Style::default().dim()); buf.set_string(area.x + 1, area.y + 1 + key, "█", - Style::default().black()); + Style::default().dim()); for step in 0..64 { let bg = if step as u32 == (beat - 1) * 16 + (beat_sub * 16.0) as u32 { + ratatui::style::Color::Gray + } else if step == cursor.1 as usize || key == cursor.0/2 { ratatui::style::Color::Black } else { ratatui::style::Color::Reset @@ -258,22 +341,22 @@ fn draw_sequence_keys ( match (top, bottom) { (true, true) => { buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "█", - Style::default().yellow().bold().bg(bg)); + Style::default().yellow().not_dim().bold().bg(bg)); }, (true, false) => { buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "▀", - Style::default().yellow().bold().bg(bg)); + Style::default().yellow().not_dim().bold().bg(bg)); }, (false, true) => { buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "▄", - Style::default().yellow().bold().bg(bg)); + Style::default().yellow().not_dim().bold().bg(bg)); }, (false, false) => if step % 16 == 0 { buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "┊", - Style::default().black().bg(bg)) + Style::default().dim().bg(bg)) } else { buf.set_string(area.x + 3 + step as u16, area.y + 1 + key, "·", - Style::default().black().bg(bg)) + Style::default().dim().bg(bg)) }, } } @@ -289,7 +372,7 @@ fn draw_sequence_keys ( } fn draw_sequence_cursor ( - area: Rect, buf: &mut Buffer, cursor: (u16, u16, u16) + area: Rect, buf: &mut Buffer, cursor: &(u16, u16, u16) ) { let cursor_character = match cursor.0 % 2 { 0 => "▀", @@ -297,11 +380,13 @@ fn draw_sequence_cursor ( _ => unreachable!() }; let cursor_y = cursor.0 / 2; - buf.set_string(area.x + cursor.1 + 3, area.y + 1 + cursor_y, if cursor.2 == 0 { + let cursor_text = if cursor.2 == 0 { cursor_character.into() } else { cursor_character.repeat(cursor.2 as usize) - }, Style::default().yellow()); + }; + let style = Style::default().yellow().dim(); + buf.set_string(area.x + cursor.1 + 3, area.y + 1 + cursor_y, cursor_text, style); } pub struct Notifications; diff --git a/src/device/transport.rs b/src/device/transport.rs index 29b7235d..82016d77 100644 --- a/src/device/transport.rs +++ b/src/device/transport.rs @@ -66,8 +66,7 @@ impl WidgetRef for Transport { draw_leaf(buf, area, 0, 28, "START"); draw_leaf(buf, area, 0, 35, "Project: Witty Gerbil - Sha Na Na "); let position = self.transport.query().expect("failed to query transport"); - draw_leaf(buf, area, 2, 0, &format!( - "BPM {:03}.{:03}", + draw_leaf(buf, area, 2, 0, &format!("BPM {:03}.{:03}", self.bpm as u64, ((self.bpm % 1.0) * 1000.0) as u64 )); @@ -75,27 +74,37 @@ impl WidgetRef for Transport { //.with_bpm(self.bpm) //.with_timesig(self.timesig.0, self.timesig.1)); //.unwrap(); - draw_leaf(buf, area, 2, 13, &format!("BBT {}.{}.{}", - 0, - 0, - 0, + let rate = position.pos.frame_rate().unwrap(); + let frame = position.pos.frame(); + let second = (frame as f64) / (rate as f64); + let minute = second / 60f64; + let bpm = 120f64; + let div = 4; + let beats = minute * bpm; + let bars = beats as u32 / div as u32; + let beat = beats as u32 % div as u32 + 1; + let beat_sub = beats % 1.0; + //buf.set_string( + //area.x - 18, area.y + area.height, + //format!("BBT {bars:04}:{beat:02}.{:02}", (beat_sub * 16.0) as u32), + //Style::default() + //); + draw_leaf(buf, area, 2, 13, &format!("BBT {bars:04}:{beat:02}.{:02}", + (beat_sub * 16.0) as u32 )); - let rate = position.pos.frame_rate().unwrap(); - let frame = position.pos.frame(); let time = frame as f64 / rate as f64; let seconds = time % 60.0; let msec = seconds % 1.0; let minutes = (time / 60.0) % 60.0; let hours = time / 3600.0; - draw_leaf(buf, area, 2, 27, &format!( - "Time {:02}:{:02}:{:02}.{:03}", + draw_leaf(buf, area, 2, 29, &format!("Time {:02}:{:02}:{:02}.{:03}", hours as u64, minutes as u64, seconds as u64, (msec * 1000.0) as u64 )); - draw_leaf(buf, area, 2, 46, &format!("Rate {:>6}Hz", rate)); - draw_leaf(buf, area, 2, 61, &format!("Frame {:>12}", frame)); + draw_leaf(buf, area, 2, 48, &format!("Rate {:>6}Hz", rate)); + draw_leaf(buf, area, 2, 63, &format!("Frame {:>10}", frame)); //Line::from("Project:").render(area, buf); //if let Ok(position) = self.transport.query() { //let frame = position.pos.frame(); diff --git a/src/main.rs b/src/main.rs index 3d2f6c26..c38c1276 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,9 +33,13 @@ fn main () -> Result<(), Box> { Some(cli::Command::Sampler) => engine.run( crate::device::sampler::Sampler::new()?, ), - Some(cli::Command::Sequencer) => engine.run( - crate::device::sequencer::Sequencer::new(Some("Sequencer"))?, - ), + Some(cli::Command::Sequencer { inputs, outputs }) => { + engine.run(crate::device::sequencer::Sequencer::new( + Some("Sequencer"), + Some(&inputs.into_iter().map(|x|x.unwrap()).collect::>()), + Some(&outputs.into_iter().map(|x|x.unwrap()).collect::>()), + )?) + }, None => engine.run(App { exited: false, mode: Mode::Sequencer, @@ -44,18 +48,26 @@ fn main () -> Result<(), Box> { )?, focus: 0, devices: vec![ - crate::device::Device::Sequencer( - crate::device::sequencer::Sequencer::new(Some("Melody#000"))? + crate::device::Device::Mixer( + crate::device::mixer::Mixer::new()? ), crate::device::Device::Sequencer( - crate::device::sequencer::Sequencer::new(Some("Rhythm#000"))? + crate::device::sequencer::Sequencer::new( + Some("Melody#000"), + None, + None + )? + ), + crate::device::Device::Sequencer( + crate::device::sequencer::Sequencer::new( + Some("Rhythm#000"), + None, + None, + )? ), crate::device::Device::Sampler( crate::device::sampler::Sampler::new()? ), - crate::device::Device::Mixer( - crate::device::mixer::Mixer::new()? - ), crate::device::Device::Looper( crate::device::looper::Looper::new()? ), @@ -95,6 +107,7 @@ impl WidgetRef for App { use ratatui::style::Stylize; let mut constraints = vec![ Constraint::Length(4), + Constraint::Length(10), Constraint::Max(18), Constraint::Max(18), Constraint::Max(0), @@ -105,9 +118,13 @@ impl WidgetRef for App { .direction(Direction::Vertical) .constraints(&constraints) .split(area); + self.transport.render(areas[0], buffer); for (index, device) in self.devices.iter().enumerate() { device.render(areas[index + 1], buffer); + if index == self.focus { + draw_focus_corners(buffer, areas[index+1]); + } } } } diff --git a/src/prelude.rs b/src/prelude.rs index a60534ac..6936ed03 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -62,4 +62,8 @@ pub use crate::engine::{ HandleInput, Event }; -pub use crate::render::{draw_box, draw_leaf}; +pub use crate::render::{ + draw_box, + draw_leaf, + draw_focus_corners +}; diff --git a/src/render.rs b/src/render.rs index f2a1002f..c4c36af4 100644 --- a/src/render.rs +++ b/src/render.rs @@ -52,6 +52,15 @@ pub fn draw_box (buffer: &mut Buffer, area: Rect) { buffer.set_string(area.x, area.y + area.height - 1, bottom, border); } +pub fn draw_focus_corners (buffer: &mut Buffer, area: Rect) { + use ratatui::style::{Style, Stylize}; + let focus = Style::default().yellow().bold().not_dim(); + buffer.set_string(area.x, area.y, "╭", focus); + buffer.set_string(area.x + area.width - 1, area.y, "╮", focus); + buffer.set_string(area.x, area.y + area.height - 1, "╰", focus); + buffer.set_string(area.x + area.width - 1, area.y + area.height - 1, "╯", focus); +} + pub fn render_toolbar_vertical ( stdout: &mut std::io::Stdout, offset: (u16, u16),