use crate::*; /////////////////////////////////////////////////////////////////////////////////////////////////// const CORNERS: CornersTall = CornersTall(NOT_DIM_GREEN); tui_style!(GRAY_DIM = Some(Color::Gray), None, None, Modifier::DIM, Modifier::empty()); tui_style!(GRAY_NOT_DIM_BOLD = Some(Color::Gray), None, None, Modifier::BOLD, Modifier::DIM); tui_style!(NOT_DIM_BOLD = None, None, None, Modifier::BOLD, Modifier::DIM); tui_style!(NOT_DIM_GREEN = Some(Color::Rgb(96, 255, 32)), Some(COLOR_BG1), None, Modifier::empty(), Modifier::DIM); tui_style!(NOT_DIM = None, None, None, Modifier::empty(), Modifier::DIM); tui_style!(WHITE_NOT_DIM_BOLD = Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); tui_style!(STYLE_LABEL = Some(Color::Reset), None, None, Modifier::empty(), Modifier::BOLD); tui_style!(STYLE_VALUE = Some(Color::White), None, None, Modifier::BOLD, Modifier::DIM); /////////////////////////////////////////////////////////////////////////////////////////////////// impl Arranger { pub fn rename_selected (&mut self) { self.modal = Some(Box::new(ArrangerRenameModal::new( self.selected, &match self.selected { ArrangerFocus::Mix => self.name.clone(), ArrangerFocus::Track(t) => self.tracks[t].name.clone(), ArrangerFocus::Scene(s) => self.scenes[s].name.clone(), ArrangerFocus::Clip(t, s) => self.tracks[t].phrases[s].read().unwrap().name.clone(), } ))); } } impl Focusable for Arranger { fn is_focused (&self) -> bool { self.focused } fn set_focused (&mut self, focused: bool) { self.focused = focused } } impl Handle for Arranger { fn handle (&mut self, from: &TuiInput) -> Perhaps { if let Some(modal) = self.modal.as_mut() { let result = modal.handle(from)?; if from.is_done() { self.modal = None; } return Ok(result) } match from.event() { // mode_switch: switch the display mode key!(KeyCode::Char('`')) => { self.mode.to_next() }, // cursor_up: move cursor up key!(KeyCode::Up) => { match self.mode { ArrangerViewMode::Horizontal => self.track_prev(), _ => self.scene_prev(), }; self.show_phrase()?; }, // cursor_down key!(KeyCode::Down) => { match self.mode { ArrangerViewMode::Horizontal => self.track_next(), _ => self.scene_next(), }; self.show_phrase()?; }, // cursor left key!(KeyCode::Left) => { match self.mode { ArrangerViewMode::Horizontal => self.scene_prev(), _ => self.track_prev(), }; self.show_phrase()?; }, // cursor right key!(KeyCode::Right) => { match self.mode { ArrangerViewMode::Horizontal => self.scene_next(), _ => self.track_next(), }; self.show_phrase()?; }, // increment: use next clip here key!(KeyCode::Char('.')) => { self.phrase_next(); }, // decrement: use previous next clip here key!(KeyCode::Char(',')) => { self.phrase_prev(); }, // decrement: use previous clip here key!(KeyCode::Enter) => { self.activate(); }, // scene_add: add a new scene key!(Ctrl-KeyCode::Char('a')) => { self.scene_add(None)?; }, // track_add: add a new scene key!(Ctrl-KeyCode::Char('t')) => { self.track_add(None)?; }, // rename: add a new scene key!(KeyCode::Char('n')) => { self.rename_selected(); }, // length: add a new scene key!(KeyCode::Char('l')) => { todo!(); }, // color: set color of item at cursor key!(KeyCode::Char('c')) => { todo!(); }, _ => return Ok(None) } Ok(Some(true)) } } impl Content for Arranger { type Engine = Tui; fn content (&self) -> impl Widget { Layers::new(move |add|{ match self.mode { ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)), ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor)) }?; add(&Align::SE(self.selected.description( &self.tracks, &self.scenes, ).as_str())) }) } } /////////////////////////////////////////////////////////////////////////////////////////////////// impl<'a> Content for VerticalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { let Self(state, factor) = self; let ppq = 96; let (cols, rows) = if *factor == 0 {( track_clip_name_lengths(state.tracks.as_slice()), scene_ppqs(state.tracks.as_slice(), state.scenes.as_slice()), )} else {( track_clip_name_lengths(state.tracks.as_slice()), (0..=state.scenes.len()).map(|i|(factor*ppq, factor*ppq*i)).collect::>(), )}; //let height = rows.last().map(|(w,y)|(y+w)/ppq).unwrap_or(16); let tracks: &[Sequencer] = state.tracks.as_ref(); let scenes: &[Scene] = state.scenes.as_ref(); let offset = 4 + scene_name_max_len(scenes) as u16; Layers::new(move |add|{ let rows: &[(usize, usize)] = rows.as_ref(); let cols: &[(usize, usize)] = cols.as_ref(); let track_titles = row!((track, (w, _)) in tracks.iter().zip(cols) => (&track.name.read().unwrap().as_str() as &dyn Widget) .min_xy(*w as u16, 2).push_x(offset)); let scene_name = |scene, playing: bool, height|row!( if playing { "▶ " } else { " " }, (scene as &Scene).name.read().unwrap().as_str(), ).fixed_xy(offset.saturating_sub(1), height); let scene_clip = |scene, track: usize, w: u16, h: u16|Layers::new(move |add|{ let mut color = Color::Rgb(40, 50, 30); match (tracks.get(track), (scene as &Scene).clips.get(track)) { (Some(track), Some(Some(clip))) => match track.phrases.get(*clip) { Some(phrase) => { let name = &(phrase as &Arc>).read().unwrap().name; let name = name.read().unwrap(); let name = format!("{clip:02} {}", name); add(&name.as_str().push_x(1))?; if (track as &Sequencer<_>).sequence == Some(*clip) { color = COLOR_PLAYING } else { color = COLOR_BG1 }; }, _ => {} }, _ => {} }; add(&Background(color)) }).fixed_xy(w, h); let tracks_clips = col!((scene, (pulses, _)) in scenes.iter().zip(rows) => { let height = 1.max((pulses / 96) as u16); let playing = scene.is_playing(tracks); Stack::right(move |add| { add(&scene_name(scene, playing, height))?; for (track, (w, _x)) in cols.iter().enumerate() { add(&scene_clip(scene, track, *w as u16, height))?; } Ok(()) }).fixed_y(height) }); add(&VerticalArrangerGrid(offset, &rows, &cols))?; add(&VerticalArrangerCursor(state.focused, state.selected, offset, &cols, &rows))?; add(&col!(track_titles, tracks_clips))?; Ok(()) }) .bg(Color::Rgb(28, 35, 25)) .border(Lozenge(Style::default() .bg(Color::Rgb(40, 50, 30)) .fg(Color::Rgb(70, 80, 50)))) } } pub fn track_clip_name_lengths (tracks: &[Sequencer]) -> Vec<(usize, usize)> { let mut total = 0; let mut lengths: Vec<(usize, usize)> = tracks.iter().map(|track|{ let len = 4 + track.phrases .iter() .fold(track.name.read().unwrap().len(), |len, phrase|{ len.max(phrase.read().unwrap().name.read().unwrap().len()) }); total = total + len; (len, total - len) }).collect(); lengths.push((0, total)); lengths } pub fn scene_ppqs (tracks: &[Sequencer], scenes: &[Scene]) -> Vec<(usize, usize)> { let mut total = 0; let mut scenes: Vec<(usize, usize)> = scenes.iter().map(|scene|{ let pulses = scene.pulses(tracks).max(96); total = total + pulses; (pulses, total - pulses) }).collect(); scenes.push((0, total)); scenes } pub fn scene_name_max_len (scenes: &[Scene]) -> usize { scenes.iter() .map(|s|s.name.read().unwrap().len()) .fold(0, usize::max) } impl<'a> Widget for VerticalArrangerGrid<'a> { type Engine = Tui; fn render (&self, to: &mut TuiOutput) -> Usually<()> { let area = to.area(); let Self(offset, rows, cols) = self; let style = Some(Style::default().fg(COLOR_SEPARATOR)); for (_, x) in cols.iter() { let x = offset + area.x() + *x as u16 - 1; for y in area.y()..area.y2() { to.blit(&"▎", x, y, style); } } for (_, y) in rows.iter() { let y = area.y() + (*y / 96) as u16 + 1; if y >= to.buffer.area.height { break } for x in area.x()..area.x2().saturating_sub(2) { if x < to.buffer.area.x && y < to.buffer.area.y { let cell = to.buffer.get_mut(x, y); cell.modifier = Modifier::UNDERLINED; cell.underline_color = COLOR_SEPARATOR; } } } Ok(()) } } impl<'a> Widget for VerticalArrangerCursor<'a> { type Engine = Tui; fn render (&self, to: &mut TuiOutput) -> Usually<()> { let area = to.area(); let Self(focused, selected, offset, cols, rows) = *self; let get_track_area = |t: usize| [ offset + area.x() + cols[t].1 as u16 - 1, area.y(), cols[t].0 as u16, area.h() ]; let get_scene_area = |s: usize| [ area.x(), 2 + area.y() + (rows[s].1 / 96) as u16, area.w(), (rows[s].0 / 96) as u16 ]; let get_clip_area = |t: usize, s: usize| [ offset + area.x() + cols[t].1 as u16 - 1, 2 + area.y() + (rows[s].1 / 96) as u16, cols[t].0 as u16, (rows[s].0 / 96) as u16 ]; let mut track_area: Option<[u16;4]> = None; let mut scene_area: Option<[u16;4]> = None; let mut clip_area: Option<[u16;4]> = None; let area = match selected { ArrangerFocus::Mix => { if focused { to.fill_bg(area, Color::Rgb(40, 50, 30)); } area }, ArrangerFocus::Track(t) => { track_area = Some(get_track_area(t)); area }, ArrangerFocus::Scene(s) => { scene_area = Some(get_scene_area(s)); area }, ArrangerFocus::Clip(t, s) => { track_area = Some(get_track_area(t)); scene_area = Some(get_scene_area(s)); clip_area = Some(get_clip_area(t, s)); area }, }; if let Some([x, y, width, height]) = track_area { to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50)); to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50)); } if let Some([_, y, _, height]) = scene_area { to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50)); to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50)); } if focused { if let Some(clip_area) = clip_area { to.render_in(clip_area, &CORNERS)?; to.fill_bg(clip_area, Color::Rgb(40, 50, 30)); } else if let Some(track_area) = track_area { to.render_in(track_area.clip_h(2), &CORNERS)?; to.fill_bg(track_area, Color::Rgb(40, 50, 30)); } else if let Some(scene_area) = scene_area { to.render_in(scene_area.clip_w(offset-1), &CORNERS)?; to.fill_bg(scene_area, Color::Rgb(40, 50, 30)); } } Ok(()) } } /////////////////////////////////////////////////////////////////////////////////////////////////// impl<'a> Content for HorizontalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { let Arranger { tracks, focused, selected, scenes, .. } = self.0; let _tracks = tracks.as_slice(); lay!( focused.then_some(Background(Color::Rgb(40, 50, 30))), row!( // name CustomWidget::new(|_|{ todo!() }, |_: &mut TuiOutput|{ todo!() //let Self(tracks, selected) = self; //let yellow = Some(Style::default().yellow().bold().not_dim()); //let white = Some(Style::default().white().bold().not_dim()); //let area = to.area(); //let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; //let offset = 0; // track scroll offset //for y in 0..area.h() { //if y == 0 { //to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2 + offset; //if let Some(track) = tracks.get(index) { //let selected = selected.track() == Some(index); //let style = if selected { yellow } else { white }; //to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; //to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; //} //} //} //Ok(Some(area)) }), // monitor CustomWidget::new(|_|{ todo!() }, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().green().bold()); //let off = Some(DIM); //area.x += 1; //for y in 0..area.h() { //if y == 0 { ////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //let style = if track.monitoring { on } else { off }; //to.blit(&" MON ", area.x(), area.y() + y, style)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // record CustomWidget::new(|_|{ todo!() }, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().red().bold()); //let off = Some(Style::default().dim()); //area.x += 1; //for y in 0..area.h() { //if y == 0 { ////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //let style = if track.recording { on } else { off }; //to.blit(&" REC ", area.x(), area.y() + y, style)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // overdub CustomWidget::new(|_|{ todo!() }, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let on = Some(Style::default().not_dim().yellow().bold()); //let off = Some(Style::default().dim()); //area.x = area.x + 1; //for y in 0..area.h() { //if y == 0 { ////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(track) = tracks.get(index) { //to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { //on //} else { //off //})?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }), // erase CustomWidget::new(|_|{ todo!() }, |_: &mut TuiOutput|{ todo!() }), // gain CustomWidget::new(|_|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let off = Some(Style::default().dim()); //area.x = area.x + 1; //for y in 0..area.h() { //if y == 0 { ////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(_) = tracks.get(index) { //to.blit(&" DEL ", area.x(), area.y() + y, off)?; //} else { //area.height = y; //break //} //} //} //area.width = 4; //Ok(Some(area)) }, |_: &mut TuiOutput|{ todo!() //let Self(tracks) = self; //let mut area = to.area(); //let off = Some(Style::default().dim()); //area.x = area.x() + 1; //for y in 0..area.h() { //if y == 0 { ////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; //} else if y % 2 == 0 { //let index = (y as usize - 2) / 2; //if let Some(_) = tracks.get(index) { //to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; //} else { //area.height = y; //break //} //} //} //area.width = 7; //Ok(Some(area)) }), // scenes CustomWidget::new(|_|{ todo!() }, |to: &mut TuiOutput|{ let Arranger { tracks, scenes, selected, .. } = self.0; let area = to.area(); let mut x2 = 0; let [x, y, _, height] = area; for (scene_index, scene) in scenes.iter().enumerate() { let active_scene = selected.scene() == Some(scene_index); let sep = Some(if active_scene { Style::default().yellow().not_dim() } else { Style::default().dim() }); for y in y+1..y+height { to.blit(&"│", x + x2, y, sep); } let name = scene.name.read().unwrap(); let mut x3 = name.len() as u16; to.blit(&*name, x + x2, y, sep); for (i, clip) in scene.clips.iter().enumerate() { let active_track = selected.track() == Some(i); if let Some(clip) = clip { let y2 = y + 2 + i as u16 * 2; let label = match tracks[i].phrases.get(*clip) { Some(phrase) => &format!("{}", phrase.read().unwrap().name.read().unwrap()), None => "...." }; to.blit(&label, x + x2, y2, Some(if active_track && active_scene { Style::default().not_dim().yellow().bold() } else { Style::default().not_dim() })); x3 = x3.max(label.len() as u16) } } x2 = x2 + x3 + 1; } //Ok(Some([x, y, x2, height])) Ok(()) }), ) ) } } pub fn track_name_max_len (tracks: &[Sequencer]) -> usize { tracks.iter() .map(|s|s.name.read().unwrap().len()) .fold(0, usize::max) } /////////////////////////////////////////////////////////////////////////////////////////////////// impl Content for ArrangerRenameModal { type Engine = Tui; fn content (&self) -> impl Widget { todo!(); Layers::new(|add|{Ok(())}) //let area = to.area(); //let y = area.y() + area.h() / 2; //let bg_area = [1, y - 1, area.w() - 2, 3]; //to.fill_bg(bg_area, COLOR_BG1); //Lozenge(Style::default().bold().white().dim()).draw(to.with_rect(bg_area)); //let label = match self.target { //ArrangerFocus::Mix => "Rename project:", //ArrangerFocus::Track(_) => "Rename track:", //ArrangerFocus::Scene(_) => "Rename scene:", //ArrangerFocus::Clip(_, _) => "Rename clip:", //}; //let style = Some(Style::default().not_bold().white().not_dim()); //to.blit(&label, area.x() + 3, y, style); //let style = Some(Style::default().bold().white().not_dim()); //to.blit(&self.value, area.x() + 3 + label.len() as u16 + 1, y, style); //let style = Some(Style::default().bold().white().not_dim().reversed()); //to.blit(&"▂", area.x() + 3 + label.len() as u16 + 1 + self.cursor as u16, y, style); //Ok(Some(area)) //Ok(()) } } impl Handle for ArrangerRenameModal { fn handle (&mut self, from: &TuiInput) -> Perhaps { match from.event() { TuiEvent::Input(Event::Key(k)) => { match k.code { KeyCode::Esc => { self.exit(); }, KeyCode::Enter => { *self.result.write().unwrap() = self.value.clone(); self.exit(); }, KeyCode::Left => { self.cursor = self.cursor.saturating_sub(1); }, KeyCode::Right => { self.cursor = self.value.len().min(self.cursor + 1) }, KeyCode::Backspace => { let last = self.value.len().saturating_sub(1); self.value = format!("{}{}", &self.value[0..self.cursor.min(last)], &self.value[self.cursor.min(last)..last] ); self.cursor = self.cursor.saturating_sub(1) } KeyCode::Char(c) => { self.value.insert(self.cursor, c); self.cursor = self.value.len().min(self.cursor + 1) }, _ => {} } Ok(Some(true)) }, _ => Ok(None), } } } /////////////////////////////////////////////////////////////////////////////////////////////////// impl Sequencer { const H_KEYS_OFFSET: usize = 5; /// Select which pattern to display. This pre-renders it to the buffer at full resolution. pub fn show (&mut self, phrase: Option<&Arc>>) -> Usually<()> { self.phrase = phrase.map(Clone::clone); if let Some(ref phrase) = self.phrase { let width = usize::MAX.min(phrase.read().unwrap().length); let mut buffer = BigBuffer::new(width, 64); let phrase = phrase.read().unwrap(); Self::fill_seq_bg(&mut buffer, phrase.length, self.ppq)?; Self::fill_seq_fg(&mut buffer, &phrase)?; self.buffer = buffer; } else { self.buffer = Default::default(); } Ok(()) } fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) -> Usually<()> { for x in 0..buf.width { if x as usize >= length { break } let style = Style::default(); buf.get_mut(x, 0).map(|cell|{ cell.set_char('-'); cell.set_style(style); }); for y in 0 .. buf.height { buf.get_mut(x, y).map(|cell|{ cell.set_char(if ppq == 0 { '·' } else if x % (4 * ppq) == 0 { '│' } else if x % ppq == 0 { '╎' } else { '·' }); cell.set_fg(Color::Gray); cell.modifier = Modifier::DIM; }); } } Ok(()) } fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) -> Usually<()> { let mut notes_on = [false;128]; for x in 0..buf.width { if x as usize >= phrase.length { break } if let Some(notes) = phrase.notes.get(x as usize) { if phrase.percussive { for note in notes { match note { MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true, _ => {} } } } else { for note in notes { match note { MidiMessage::NoteOn { key, .. } => notes_on[key.as_int() as usize] = true, MidiMessage::NoteOff { key, .. } => notes_on[key.as_int() as usize] = false, _ => {} } } } for y in 0..buf.height/2 { if y >= 64 { break } if let Some(block) = half_block( notes_on[y as usize * 2], notes_on[y as usize * 2 + 1], ) { buf.get_mut(x, y).map(|cell|{ cell.set_char(block); cell.set_fg(Color::White); }); } } if phrase.percussive { notes_on.fill(false); } } } Ok(()) } pub(crate) fn style_focus (&self) -> Option