systematizing jack handlers

This commit is contained in:
🪞👃🪞 2024-06-14 16:54:28 +03:00
parent d627d257ad
commit 4ae62c5bc2
10 changed files with 342 additions and 208 deletions

View file

@ -165,6 +165,10 @@ pub enum EngineEvent {
Update,
/// Update the whole form.
Redraw,
/// Device gains focus
Focus,
/// Device loses focus
Blur,
}
pub fn activate_jack_client <N: NotificationHandler + Sync + 'static> (

View file

@ -14,6 +14,13 @@ impl Chain {
}
}
pub fn process (
client: &Client,
scope: &ProcessScope
) -> Control {
Control::Continue
}
pub fn render (state: &Chain, buf: &mut Buffer, area: Rect)
-> Usually<Rect>
{

View file

@ -12,6 +12,13 @@ impl Launcher {
}
}
pub fn process (
client: &Client,
scope: &ProcessScope
) -> Control {
Control::Continue
}
pub fn render (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
Ok(Rect::default())
}

View file

@ -12,6 +12,13 @@ impl Looper {
}
}
pub fn process (
client: &Client,
scope: &ProcessScope
) -> Control {
Control::Continue
}
pub fn render (state: &Looper, buf: &mut Buffer, area: Rect)
-> Usually<Rect>
{

View file

@ -2,7 +2,7 @@ use crate::prelude::*;
pub struct Mixer {
name: String,
jack: Jack<Notifications>,
jack: Jack<MixerJack>,
tracks: Vec<Track>,
selected_track: usize,
selected_column: usize,
@ -14,14 +14,9 @@ impl Mixer {
name,
ClientOptions::NO_START_SERVER
)?;
let jack = client.activate_async(
Notifications,
ClosureProcessHandler::new(Box::new(
move |_: &Client, _: &ProcessScope| -> Control {
Control::Continue
}) as Box<dyn FnMut(&Client, &ProcessScope)->Control + Send>
)
)?;
let jack = MixerJack.activate(client, ClosureProcessHandler::new(Box::new(
move|client: &Client, scope: &ProcessScope|process(client, scope)
) as BoxedProcessHandler))?;
Ok(DynamicDevice::new(render, handle, |_|{}, Self {
name: name.into(),
selected_column: 0,
@ -41,6 +36,13 @@ impl Mixer {
}
}
pub fn process (
client: &Client,
scope: &ProcessScope
) -> Control {
Control::Continue
}
pub fn render (state: &Mixer, buf: &mut Buffer, mut area: Rect)
-> Usually<Rect>
{
@ -211,9 +213,17 @@ impl Track {
}
}
pub struct Notifications;
pub struct MixerJack;
impl NotificationHandler for Notifications {
impl MixerJack {
fn activate <P> (self, client: Client, handler: P) -> Usually<AsyncClient<Self, P>>
where P: 'static + Send + ::jack::ProcessHandler
{
Ok(client.activate_async(self, handler)?)
}
}
impl NotificationHandler for MixerJack {
fn thread_init (&self, _: &Client) {
}
@ -254,7 +264,7 @@ impl NotificationHandler for Notifications {
//fn handle (&mut self, engine: &mut TUI<W>) -> Result<Option<bool>> {
//Ok(None)
//}
//}
//
//impl<W: Write> Output<TUI<W>, [u16;2]> for Mixer {
//fn render (&self, envine: &mut TUI<W>) -> Result<Option<[u16;2]>> {

View file

@ -7,7 +7,7 @@ pub const ACTIONS: [(&'static str, &'static str);2] = [
pub struct Sampler {
name: String,
jack: Jack<self::Notifications>,
jack: Jack<SamplerJack>,
samples: Arc<Mutex<Vec<Sample>>>,
selected_sample: usize,
selected_column: usize,
@ -23,57 +23,65 @@ impl Sampler {
];
let samples = Arc::new(Mutex::new(samples));
let input = client.register_port("trigger", ::jack::MidiIn::default())?;
let handler: BoxedProcessHandler = Box::new({
let exited = exited.clone();
let jack = SamplerJack.activate(client, ClosureProcessHandler::new({
let exited = exited.clone();
let samples = samples.clone();
Box::new(move |_: &Client, scope: &ProcessScope| -> Control {
if exited.fetch_and(true, Ordering::Relaxed) {
return Control::Quit
}
let mut samples = samples.lock().unwrap();
for event in input.iter(scope) {
let len = 3.min(event.bytes.len());
let mut data = [0; 3];
data[..len].copy_from_slice(&event.bytes[..len]);
if (data[0] >> 4) == 0b1001 { // note on
let channel = data[0] & 0b00001111;
let note = data[1];
let velocity = data[2];
for sample in samples.iter_mut() {
if /*sample.trigger.0 == channel &&*/ sample.trigger.1 == note {
sample.play(velocity);
}
}
}
for sample in samples.iter_mut() {
if let Some(playing) = sample.playing {
for (index, value) in sample.port.as_mut_slice(scope).iter_mut().enumerate() {
*value = *sample.data[0].get(playing + index).unwrap_or(&0f32);
}
if playing + scope.n_frames() as usize > sample.data[0].len() {
sample.playing = None
} else {
sample.playing = Some(playing + scope.n_frames() as usize)
}
}
}
}
Box::new(move |client: &Client, scope: &ProcessScope|{
process(client, scope, &exited, &samples, &input);
Control::Continue
})
});
}) as BoxedProcessHandler
}))?;
Ok(DynamicDevice::new(render, handle, |_|{}, Self {
name: name.into(),
selected_sample: 0,
selected_column: 0,
samples,
jack: client.activate_async(
self::Notifications,
ClosureProcessHandler::new(handler)
)?,
jack,
}))
}
}
pub fn process (
client: &Client,
scope: &ProcessScope,
exited: &Arc<AtomicBool>,
samples: &Arc<Mutex<Vec<Sample>>>,
input: &Port<MidiIn>
) -> Control {
if exited.fetch_and(true, Ordering::Relaxed) {
return Control::Quit
}
let mut samples = samples.lock().unwrap();
for event in input.iter(scope) {
let len = 3.min(event.bytes.len());
let mut data = [0; 3];
data[..len].copy_from_slice(&event.bytes[..len]);
if (data[0] >> 4) == 0b1001 { // note on
let channel = data[0] & 0b00001111;
let note = data[1];
let velocity = data[2];
for sample in samples.iter_mut() {
if /*sample.trigger.0 == channel &&*/ sample.trigger.1 == note {
sample.play(velocity);
}
}
}
for sample in samples.iter_mut() {
if let Some(playing) = sample.playing {
for (index, value) in sample.port.as_mut_slice(scope).iter_mut().enumerate() {
*value = *sample.data[0].get(playing + index).unwrap_or(&0f32);
}
if playing + scope.n_frames() as usize > sample.data[0].len() {
sample.playing = None
} else {
sample.playing = Some(playing + scope.n_frames() as usize)
}
}
}
}
Control::Continue
}
pub struct Sample {
port: Port<AudioOut>,
name: String,
@ -217,9 +225,17 @@ pub fn handle (state: &mut Sampler, event: &EngineEvent) -> Result<(), Box<dyn E
Ok(())
}
pub struct Notifications;
pub struct SamplerJack;
impl NotificationHandler for Notifications {
impl SamplerJack {
fn activate <P> (self, client: Client, handler: P) -> Usually<AsyncClient<Self, P>>
where P: 'static + Send + ::jack::ProcessHandler
{
Ok(client.activate_async(self, handler)?)
}
}
impl NotificationHandler for SamplerJack {
fn thread_init (&self, _: &Client) {
}

View file

@ -3,6 +3,7 @@ use ratatui::style::Stylize;
pub struct Sequencer {
name: String,
jack: Jack<SequencerJack>,
playing: Arc<AtomicBool>,
recording: Arc<AtomicBool>,
overdub: Arc<AtomicBool>,
@ -10,9 +11,16 @@ pub struct Sequencer {
outputs_open: Arc<AtomicBool>,
cursor: (u16, u16, u16),
timesig: (f32, f32),
pub jack_client: Jack<self::Notifications>,
sequence: Arc<Mutex<Vec<Vec<Option<SequencerEvent>>>>>,
sequences: Arc<Mutex<Vec<MIDISequence>>>,
mode: SequencerMode,
}
pub enum SequencerMode {
Tiny,
Compact,
Vertical,
Horizontal,
}
impl Sequencer {
@ -31,15 +39,6 @@ impl Sequencer {
let playing = Arc::new(AtomicBool::new(true));
let recording = Arc::new(AtomicBool::new(true));
let overdub = Arc::new(AtomicBool::new(false));
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<Mutex<Vec<Vec<Option<SequencerEvent>>>>> = Arc::new(
Mutex::new(vec![vec![None;64];128])
);
@ -53,7 +52,17 @@ impl Sequencer {
for (index, frame) in step_frames.iter().enumerate() {
frame_steps[*frame] = Some(index);
}
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()
)?;
Ok(DynamicDevice::new(render, handle, |_|{}, Self {
mode: SequencerMode::Vertical,
name: name.into(),
playing: playing.clone(),
recording: recording.clone(),
@ -65,67 +74,70 @@ impl Sequencer {
])),
cursor: (11, 0, 0),
timesig: (4.0, 4.0),
jack_client: crate::device::activate_jack_client(
client,
Notifications,
Box::new(move |client: &Client, scope: &ProcessScope| -> Control {
if exited.fetch_and(true, Ordering::Relaxed) {
return Control::Quit
}
if client.transport().query_state().unwrap() == TransportState::Rolling {
let chunk_start = scope.last_frame_time();
let chunk_size = scope.n_frames();
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;
// 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 {
SequencerEvent::NoteOn(pitch, velocity) => [
0b10010000,
*pitch,
*velocity
],
SequencerEvent::NoteOff(pitch) => [
0b10000000,
*pitch,
0b00000000
],
}
}).unwrap()
}
}
}
}
}
}
Control::Continue
})
)?
jack: SequencerJack.activate(client, ClosureProcessHandler::new(
Box::new(move |client: &Client, scope: &ProcessScope| process(client, scope)) as BoxedProcessHandler
))?
}))
}
}
pub fn process (
client: &Client,
scope: &ProcessScope
) -> Control {
//if exited.fetch_and(true, Ordering::Relaxed) {
//return Control::Quit
//}
//if client.transport().query_state().unwrap() == TransportState::Rolling {
//let chunk_start = scope.last_frame_time();
//let chunk_size = scope.n_frames();
//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;
//// 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 {
//SequencerEvent::NoteOn(pitch, velocity) => [
//0b10010000,
//*pitch,
//*velocity
//],
//SequencerEvent::NoteOff(pitch) => [
//0b10000000,
//*pitch,
//0b00000000
//],
//}
//}).unwrap()
//}
//}
//}
//}
//}
//}
Control::Continue
}
pub const ACTIONS: [(&'static str, &'static str);4] = [
("+/-", "Zoom"),
("A/D", "Add/delete note"),
@ -135,6 +147,9 @@ pub const ACTIONS: [(&'static str, &'static str);4] = [
pub fn handle (state: &mut Sequencer, event: &EngineEvent) -> Result<(), Box<dyn Error>> {
if let EngineEvent::Focus = event {
}
if let EngineEvent::Input(Event::Key(event)) = event {
match event.code {
KeyCode::Down => {
@ -197,33 +212,38 @@ pub fn handle (state: &mut Sequencer, event: &EngineEvent) -> Result<(), Box<dyn
Ok(())
}
const NOTE_NAMES: [&'static str;12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
const KEYS_VERTICAL: [&'static str; 6] = [
"", "", "", "", "", "",
];
fn render (sequencer: &Sequencer, buf: &mut Buffer, mut area: Rect)
-> Usually<Rect>
{
let style = Style::default().gray();
let header = draw_sequencer_header(sequencer, buf, area)?;
let piano = draw_sequencer_vertical(sequencer, buf, Rect {
x: area.x,
y: area.y + header.height,
width: area.width,
height: area.height - header.height
})?;
let area = Rect {
let piano = match sequencer.mode {
SequencerMode::Tiny => Rect {
x: area.x,
y: area.y,
width: area.width,
height: 0
},
SequencerMode::Compact => Rect {
x: area.x,
y: area.y,
width: area.width,
height: 0
},
SequencerMode::Vertical => draw_sequencer_vertical(sequencer, buf, Rect {
x: area.x,
y: area.y + header.height,
width: area.width,
height: area.height - header.height
})?,
_ => unimplemented!()
};
//draw_box_styled(buf, area, Some(Style::default().red()));
Ok(Rect {
x: area.x,
y: area.y,
width: header.width.max(piano.width),
height: header.height + piano.height
};
//draw_box_styled(buf, area, Some(Style::default().red()));
Ok(area)
})
}
fn draw_sequencer_header (sequencer: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
@ -235,32 +255,52 @@ fn draw_sequencer_header (sequencer: &Sequencer, buf: &mut Buffer, area: Rect) -
Ok(Rect { x, y, width, height: 4 })
}
const KEY_WHITE: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: ::ratatui::style::Modifier::empty(),
sub_modifier: ::ratatui::style::Modifier::empty(),
};
const KEY_BLACK: Style = Style {
fg: Some(Color::Black),
bg: None,
underline_color: None,
add_modifier: ::ratatui::style::Modifier::empty(),
sub_modifier: ::ratatui::style::Modifier::empty(),
};
const KEY_HORIZONTAL_STYLE: [Style;12] = [
KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE,
KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE,
];
fn draw_sequencer_vertical (sequencer: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
let Rect { x, y, .. } = area;
let white = Style::default().gray();
let black = Style::default().black();
for i in 0..area.width-4 {
let color = match i % 12 {
0 => white,
1 => black,
2 => white,
3 => black,
4 => white,
5 => white,
6 => black,
7 => white,
8 => black,
9 => white,
10 => black,
11 => white,
_ => unreachable!()
};
buf.set_string(x + 2 + i, y - 1, &format!(""), color);
buf.set_string(x + 2 + i, y, &format!(""), color);
for i in 0..area.width-7 {
let color = KEY_HORIZONTAL_STYLE[i as usize % 12];
buf.set_string(x + 5 + i, y - 1, &format!(""), color);
buf.set_string(x + 5 + i, y, &format!(""), color);
}
let bg = Style::default().on_black();
for i in 0..area.height - 8 {
buf.set_string(x + 2, y + 1 + i, &" ".repeat((area.width-4)as usize), bg);
buf.set_string(x + 5, y + 1 + i, &" ".repeat((area.width-7)as usize), bg);
if i % 4 == 0 {
buf.set_string(x + 2, y + 1 + i, &format!("{:2}", i / 4 + 1), Style::default());
}
}
for octave in -2i16..=8i16 {
let x = x + 5 + (24i16 + octave * 12) as u16;
if x >= area.x + area.width - 6 {
break
}
buf.set_string(
x,
y + 1,
&format!("C{octave}"),
Style::default()
);
}
Ok(Rect { x, y, width: area.width, height: area.height - 6 })
}
@ -373,6 +413,14 @@ fn draw_rec_dub_button (
buf.set_string(x + 11, y + 2, "", Style::default().gray());
}
const NOTE_NAMES: [&'static str;12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
const KEYS_VERTICAL: [&'static str; 6] = [
"", "", "", "", "", "",
];
fn draw_sequence_keys (
area: Rect,
buf: &mut Buffer,
@ -470,9 +518,17 @@ fn draw_sequence_cursor (
buf.set_string(area.x + cursor.1 + 3, area.y + 1 + cursor_y, cursor_text, style);
}
pub struct Notifications;
pub struct SequencerJack;
impl NotificationHandler for Notifications {
impl SequencerJack {
fn activate (self, client: Client, handler: ClosureProcessHandler<BoxedProcessHandler>)
-> Usually<AsyncClient<Self, ClosureProcessHandler<BoxedProcessHandler>>>
{
Ok(client.activate_async(self, handler)?)
}
}
impl NotificationHandler for SequencerJack {
fn thread_init (&self, _: &Client) {
}

View file

@ -47,50 +47,74 @@ pub fn render (state: &Transport, buf: &mut Buffer, mut area: Rect)
area.width = area.width.min(80);
area.height = 5;
//draw_box(buf, area);
draw_leaf(buf, area, 1, 0, "REC");
draw_leaf(buf, area, 1, 5, "DUB");
draw_leaf(buf, area, 1, 10, "STOP");
draw_leaf(buf, area, 1, 16, "PLAY/PAUSE");
draw_leaf(buf, area, 1, 28, "START");
draw_leaf(buf, area, 1, 35, "Project: Witty Gerbil - Sha Na Na ");
draw_leaf(buf, area, 3, 0, &format!("BPM {:03}.{:03}",
state.bpm as u64,
((state.bpm % 1.0) * 1000.0) as u64
));
let position = state.transport.as_ref().map(|t|t.query());
if let Some(Ok(position)) = position {
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, 3, 13, &format!("BBT {bars:04}:{beat:02}.{:02}",
(beat_sub * 16.0) as u32
));
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, 3, 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, 3, 48, &format!("Rate {:>6}Hz", rate));
draw_leaf(buf, area, 3, 63, &format!("Frame {:>10}", frame));
let label = Style::default().white().not_dim();
let border = Style::default().gray().dim();
let mut x = 2;
for button in [
"PLAY",
"STOP",
"REC",
"DUB",
"0.0.00",
"0:00.000",
&format!("BPM {:03}.{:03}",
state.bpm as u64,
((state.bpm % 1.0) * 1000.0) as u64
)
].iter() {
buf.set_string(area.x + x, area.y + 2, button, label);
x = x + button.len() as u16 + 1;
buf.set_string(area.x + x, area.y + 1, "", border);
buf.set_string(area.x + x, area.y + 2, "", border);
buf.set_string(area.x + x, area.y + 3, "", border);
x = x + 2;
}
//buf.set_string(area.x, area.y + 5, "Witty Gerbil - Sha Na Na", label.bold());
//&format!(" │ 00:00.00 / 00:00.00"), style);
//draw_leaf(buf, area, 1, 0, "REC");
//draw_leaf(buf, area, 1, 5, "DUB");
//draw_leaf(buf, area, 1, 10, "STOP");
//draw_leaf(buf, area, 1, 16, "PLAY/PAUSE");
//draw_leaf(buf, area, 1, 28, "START");
//draw_leaf(buf, area, 1, 35, "Project: Witty Gerbil - Sha Na Na ");
//draw_leaf(buf, area, 3, 0, &format!("BPM {:03}.{:03}",
//state.bpm as u64,
//((state.bpm % 1.0) * 1000.0) as u64
//));
//let position = state.transport.as_ref().map(|t|t.query());
//if let Some(Ok(position)) = position {
//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, 3, 13, &format!("BBT {bars:04}:{beat:02}.{:02}",
//(beat_sub * 16.0) as u32
//));
//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, 3, 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, 3, 48, &format!("Rate {:>6}Hz", rate));
//draw_leaf(buf, area, 3, 63, &format!("Frame {:>10}", frame));
//}
//let bbt = position.pos.bbt().map(|mut bbt|*bbt
//.with_bpm(state.bpm)
//.with_timesig(state.timesig.0, state.timesig.1));

View file

@ -18,6 +18,7 @@ impl Device for Rows {
match event {
EngineEvent::Input(Event::Key(KeyEvent { code: KeyCode::Esc, .. })) => {
self.focused = true;
self.items[self.focus].handle(&EngineEvent::Blur)?;
Ok(())
},
_ => self.items[self.focus].handle(event)
@ -39,9 +40,11 @@ impl Device for Rows {
},
Event::Key(KeyEvent { code: KeyCode::Enter, .. }) => {
self.focused = false;
self.items[self.focus].handle(&EngineEvent::Focus)?;
},
Event::Key(KeyEvent { code: KeyCode::Esc, .. }) => {
self.focused = true;
self.items[self.focus].handle(&EngineEvent::Blur)?;
},
_ => {
println!("{event:?}");
@ -63,7 +66,7 @@ impl Device for Rows {
height: area.height - h
})?;
if self.focused && i == self.focus {
draw_box_styled(buf, result, Some(Style::default().gray().bold().not_dim()))
draw_box_styled(buf, result, Some(Style::default().green().dim().bold()))
}
w = w.max(result.width);
h = h + result.height;

View file

@ -57,6 +57,6 @@ pub use crate::render::*;
pub use crate::device::{
Device,
DynamicDevice,
EngineEvent
EngineEvent,
};
pub type Usually<T> = Result<T, Box<dyn Error>>;