use super::*;
use crate::hotkeys::{key, label as hk};
impl App {
pub(super) fn font_for(rows: usize) -> Font {
match rows {
43.. => Font::vga_8x8(),
28 => Font::vga_8x14(),
_ => Font::vga_8x16(),
}
}
pub(super) fn view_cols(&self) -> usize {
if let Some(v) =
self.art.as_ref().filter(|_| !self.help_open && !self.settings_open && self.art_picker.is_none())
{
return v.cols();
}
match &self.session {
Some(s) if !self.dialer_open && !self.settings_open && !self.help_open => s.term.screen.cols(),
_ => COLS,
}
}
pub(super) fn cell_width(&self) -> usize {
if self.settings.nine_dot { 9 } else { 8 }
}
pub(super) fn draw_status(&mut self) {
let theme = ui::theme::current();
let (fg, bg) = (theme.status.fg(), theme.status.bg());
let cols = self.view_cols();
if self.status.cols() != cols {
self.status = Screen::new(cols, 1, 0);
}
let s = &mut self.status;
s.fill_rect(0, 0, cols, 1, Cell::new(b' ', fg, bg));
if let Some(v) =
self.art.as_ref().filter(|_| !self.help_open && !self.settings_open && self.art_picker.is_none())
{
return ui::text(s, 0, 0, cols, &v.status(), fg, bg);
}
if self.art_picker.is_some() && !self.help_open && !self.settings_open {
let what = if self.picking_script { " Run a Script" } else { " View ANSI Art" };
return ui::text(s, 0, 0, cols, &format!("{what} \u{2502} Esc returns"), fg, bg);
}
if self.help_open || self.settings_open {
let line = if self.help_open {
" Help \u{2502} Any key returns"
} else {
" Settings \u{2502} Changes are saved as you make them \u{2502} Esc returns"
};
return ui::text(s, 0, 0, cols, line, fg, bg);
}
let notice = self.notice.as_ref().filter(|(_, until)| Instant::now() < *until).map(|(m, _)| m.clone());
if self.dialer_open {
let help = notice.unwrap_or_else(|| self.dialer.status_help().to_string());
return ui::text(s, 0, 0, cols, &format!(" {help}"), fg, bg);
}
let Some(session) = &self.session else {
let help =
notice.unwrap_or_else(|| format!("{} Dialing directory {} Help {} Exit", hk("D"), hk("Z"), hk("X")));
return ui::text(s, 0, 0, cols, &format!(" Not connected \u{2502} {help}"), fg, bg);
};
let screen = &session.term.screen;
if self.scroll > 0 {
let line = format!(
" SCROLLBACK {}/{} \u{2191}\u{2193} PgUp PgDn Home Any other key returns",
self.scroll,
screen.scrollback_len()
);
return ui::text(s, 0, 0, cols, &line, theme.status_alt.fg(), theme.status_alt.bg());
}
let e = &session.entry;
if let Some(t) = session.transfer() {
let line =
format!(" {} \u{2502} {} transfer in progress \u{2502} Esc cancels", truncate(&e.name, 30), t.name());
return ui::text(s, 0, 0, cols, &line, fg, bg);
}
if let Some(l) = &self.login {
let line = if l.rip {
format!(" {} asks \u{2502} Enter answer Esc cancel", truncate(&e.name, 40))
} else {
format!(" Logging in to {} \u{2502} Enter log in Esc cancel", truncate(&e.name, 40))
};
return ui::text(s, 0, 0, cols, &line, fg, bg);
}
if let Some(r) = self.redial.as_ref().filter(|_| session.state == State::Closed) {
let why = if session.error.is_some() { "No connection" } else { "Busy" };
let secs = r.at.saturating_duration_since(Instant::now()).as_secs() + 1;
let line = format!(
" {why} \u{2502} Calling again in {secs}s (redial {} of {MAX_REDIALS}) \u{2502} Esc cancel {} now",
r.attempt,
hk("R")
);
return ui::text(s, 0, 0, cols, &line, fg, bg);
}
let line = match session.state {
State::Connecting if self.redials > 0 => {
format!(" Redialing {} (redial {} of {MAX_REDIALS})... {} cancel", e.address(), self.redials, hk("H"))
}
State::Connecting => format!(" Connecting to {}... {} cancel", e.address(), hk("H")),
State::Closed => match &session.error {
Some(err) => format!(" Connection failed: {err} \u{2502} {} Call again", hk("R")),
None => {
format!(
" Disconnected \u{2502} {HOTKEY}: {} Call again {} Directory {} Clear {} Back",
key("R"),
key("D"),
key("H"),
key("B")
)
}
},
State::Connected => {
let t = session.elapsed().as_secs();
let mut flags = String::new();
if screen.ice_colors {
flags.push_str(" iCE");
}
if session.capture_path().is_some() {
flags.push_str(" CAP");
}
if self.script.is_some() {
flags.push_str(" SCRIPT");
}
if e.baud > 0 {
flags.push_str(&format!(" {}bps", e.baud));
}
let right = notice.unwrap_or_else(|| {
format!(
"{HOTKEY}: {} Dir {} Hangup {} Upload {} Help {} Exit",
key("D"),
key("H"),
key("U"),
key("Z"),
key("X")
)
});
format!(
" {} \u{2502} {:02}:{:02}:{:02}{} \u{2502} {right}",
truncate(&e.name, 16),
t / 3600,
t / 60 % 60,
t % 60,
flags
)
}
};
ui::text(s, 0, 0, cols, &line, fg, bg);
}
pub(super) fn redraw(&mut self) {
self.draw_status();
let dialer_cursor =
if self.dialer_open { self.dialer.draw(&mut self.dialer_screen, &self.directory) } else { None };
let mut page_cursor = None;
let splash = self.splash_until.is_some();
if splash {
ui::splash::draw(&mut self.page_screen);
} else if self.help_open {
ui::help::draw(&mut self.page_screen);
} else if self.settings_open {
let own = self.own_crt_mode().map(|m| m.name());
page_cursor = self.settings_ui.draw(&mut self.page_screen, &self.settings, own, self.update.as_deref());
} else if let Some(picker) = self.art_picker.as_mut() {
page_cursor = if self.picking_script {
art_ui::draw_picker_titled(&mut self.page_screen, picker, "Run a Script", "Enter run")
} else {
art_ui::draw_picker(&mut self.page_screen, picker)
};
}
let crt = self.crt_mode();
let Some(gfx) = self.gfx.as_mut() else { return };
let size = gfx.window.inner_size();
let (Some(w), Some(h)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) else { return };
let mut overlay_cursor = None;
if let Some(session) = &self.session {
let sc = &session.term.screen;
if (self.overlay.cols(), self.overlay.rows()) != (sc.cols(), sc.rows()) {
self.overlay = Screen::new(sc.cols(), sc.rows(), 0);
}
}
let overlay = match &self.session {
Some(session)
if self.login.is_some()
|| self.save_prompt.is_some()
|| self.upload_prompt.is_some()
|| self.download_prompt.is_some()
|| session.transfer().is_some() =>
{
let sc = &session.term.screen;
for y in 0..sc.rows() {
for (x, cell) in sc.row(y).iter().enumerate() {
if let Some(c) = self.overlay.cell_mut(x, y) {
*c = *cell;
}
}
}
if let Some(login) = &self.login {
overlay_cursor = Some(login.draw(&mut self.overlay));
} else if let Some(prompt) = &self.save_prompt {
overlay_cursor = Some(prompt.draw(&mut self.overlay));
} else if let Some(prompt) = &mut self.upload_prompt {
overlay_cursor = prompt.draw(&mut self.overlay);
} else if let Some(prompt) = &self.download_prompt {
overlay_cursor = prompt.draw(&mut self.overlay);
} else if let Some(t) = session.transfer() {
transfer_ui::draw_progress(&mut self.overlay, t.name(), t.progress());
}
true
}
_ => false,
};
let mut has_status = true;
let frame = match &self.session {
_ if splash || self.help_open || self.settings_open || self.art_picker.is_some() => {
let mut rows: Vec<&[Cell]> = (0..settings_ui::ROWS).map(|y| self.page_screen.row(y)).collect();
rows.push(self.status.row(0));
Frame::plain(rows, page_cursor, true)
}
_ if self.art.is_some() => {
let v = self.art.as_ref().expect("checked");
let mut rows = v.rows();
rows.push(self.status.row(0));
let sc = &v.art.term.screen;
Frame { look: &sc.look, look_rows: art_ui::ROWS, ..Frame::plain(rows, None, sc.ice_colors) }
}
_ if self.dialer_open => {
let mut rows: Vec<&[Cell]> = (0..dialer_ui::ROWS).map(|y| self.dialer_screen.row(y)).collect();
rows.push(self.status.row(0));
Frame::plain(rows, dialer_cursor, true)
}
Some(session) if overlay => {
let mut rows: Vec<&[Cell]> = (0..self.overlay.rows()).map(|y| self.overlay.row(y)).collect();
has_status = !session.entry.hide_status;
if has_status {
rows.push(self.status.row(0));
}
let sc = &session.term.screen;
let graphics = bbs_graphics(session);
Frame {
look: &sc.look,
look_rows: sc.rows(),
graphics,
tiles: Some(&sc.tiles),
..Frame::plain(rows, overlay_cursor, sc.ice_colors)
}
}
Some(session) => {
let sc = &session.term.screen;
let mut rows = terminal_rows(session, self.scroll);
has_status = !session.entry.hide_status;
if has_status {
rows.push(self.status.row(0));
}
let live = self.scroll == 0 || sc.scrollback_len() == 0;
let cursor = (live && sc.cursor_visible && session.state == State::Connected).then(|| sc.cursor());
let selection = self.selection;
let graphics = bbs_graphics(session).filter(|_| self.scroll == 0);
Frame {
selection,
look: &sc.look,
look_rows: sc.rows(),
graphics,
tiles: Some(&sc.tiles),
..Frame::plain(rows, cursor, sc.ice_colors)
}
}
None => {
let mut rows: Vec<&[Cell]> = (0..ROWS).map(|y| self.idle_screen.row(y)).collect();
rows.push(self.status.row(0));
Frame::plain(rows, None, true)
}
};
let confirm_screen;
let frame = if self.confirm_exit {
let n = frame.rows.len() - usize::from(has_status);
let cols = frame.rows.first().map_or(COLS, |r| r.len());
let mut sc = Screen::new(cols, n, 0);
for (y, row) in frame.rows[..n].iter().enumerate() {
for (x, c) in row.iter().enumerate() {
if let Some(cell) = sc.cell_mut(x, y) {
*cell = *c;
}
}
}
let msg = match &self.session {
Some(s) if s.state == State::Connected => format!("This hangs up the call to {}.", s.entry.name),
_ => String::new(),
};
ui::confirm_box(&mut sc, "Exit BadTerm?", &msg, &[("Y", "Exit"), ("N", "Stay")]);
confirm_screen = sc;
let mut rows: Vec<&[Cell]> = (0..n).map(|y| confirm_screen.row(y)).collect();
rows.extend(frame.rows.get(n).copied());
Frame { rows, cursor: None, ..frame }
} else {
frame
};
let ui_look = ui::theme::ui_look();
let frame = Frame { ui_look: &ui_look, ..frame };
let text_rows = frame.rows.len() - usize::from(has_status);
let cols = frame.rows.first().map_or(COLS, |r| r.len());
let cell_w = if self.settings.nine_dot { 9 } else { 8 };
let base = if frame.look_rows > 0 && Font::eight_only(frame.look.fonts[0]) {
Font::vga_8x8()
} else {
Self::font_for(text_rows)
};
self.renderer.set_layout(base, cell_w, cols, frame.rows.len());
self.renderer.draw(&frame, (self.tick / 2).is_multiple_of(2), self.tick.is_multiple_of(2));
if self.flash_until.is_some() {
self.renderer.flash(text_rows);
}
if let Err(e) = gfx.surface.resize(w, h) {
return log::error!("resizing surface: {e}");
}
match gfx.surface.buffer_mut() {
Ok(mut buf) => {
let fill = gfx.window.fullscreen().is_some() || gfx.window.is_maximized();
let scaling = match self.settings.scaling {
ScaleMode::Auto if fill => Scaling::Fill,
ScaleMode::Auto | ScaleMode::Integer => Scaling::Integer,
ScaleMode::Fit => Scaling::Fill,
ScaleMode::Stretch => Scaling::Stretch,
};
let pixel_aspect = match self.settings.aspect.ratio() {
Some(ratio) => {
let text_h = (self.renderer.native_height() * text_rows / frame.rows.len()) as f64;
self.renderer.native_width() as f64 / text_h / ratio
}
None => 1.0,
};
let (ww, wh) = (w.get() as usize, h.get() as usize);
let want = match &self.bezel {
Some(b) => b.shape(),
None => {
let (nw, nh) = (self.renderer.native_width() as f64, self.renderer.native_height() as f64);
nw / (nh * pixel_aspect)
}
};
self.shape.want(want, self.settings.scaling == ScaleMode::Fit && !fill);
if let Some(b) = &self.bezel
&& self.bezel_frame.as_ref().is_none_or(|(size, _)| *size != (ww, wh))
{
self.bezel_frame = Some(((ww, wh), b.render(ww, wh)));
}
match self.bezel_frame.as_ref().filter(|_| self.bezel.is_some()) {
Some((_, frame)) => {
let (sw, sh) = (frame.screen.2.max(1) as f64, frame.screen.3.max(1) as f64);
let (nw, nh) = (self.renderer.native_width() as f64, self.renderer.native_height() as f64);
let (scanlines, curvature) = (self.settings.scanlines, self.settings.curvature);
let display = Display {
glow: self.settings.glow,
scaling: Scaling::Fill,
pixel_aspect: sh / sw * nw / nh,
crt,
scanlines,
curvature,
};
self.view_rect = self.renderer.blit_in(&mut buf, ww, wh, frame.screen, &display);
bezel::composite(&mut buf, &frame.pixels);
}
None => {
let (scanlines, curvature) = (self.settings.scanlines, self.settings.curvature);
let glow = self.settings.glow;
let display = Display { scaling, pixel_aspect, crt, glow, scanlines, curvature };
self.view_rect = self.renderer.blit(&mut buf, ww, wh, &display);
}
}
if let Err(e) = buf.present() {
log::error!("presenting frame: {e}");
}
}
Err(e) => log::error!("getting frame buffer: {e}"),
}
}
}
fn bbs_graphics(session: &Session) -> Option<crate::render::Graphics<'_>> {
if let Some(igs) = session.term.igs() {
return Some(crate::render::Graphics {
pixels: igs.pixels(),
width: igs.width(),
height: igs.height(),
palette: igs.palette(),
});
}
if let Some(sky) = session.term.skypix() {
return Some(crate::render::Graphics {
pixels: sky.pixels(),
width: badterm_core::skypix::WIDTH,
height: badterm_core::skypix::HEIGHT,
palette: sky.palette(),
});
}
let rip = session.term.rip()?;
Some(crate::render::Graphics {
pixels: rip.pixels(),
width: badterm_core::rip::WIDTH,
height: badterm_core::rip::HEIGHT,
palette: rip.palette(),
})
}