| 1 | //! Build Output: Keil-style messages, the listing and the Intel HEX file. |
| 2 | |
| 3 | use bench51_core::syntax::tokens; |
| 4 | use eframe::egui::text::LayoutJob; |
| 5 | use eframe::egui::{self, Align, CursorIcon, FontId, Layout, RichText, Sense, TextFormat, Ui}; |
| 6 | |
| 7 | use crate::state::{Action, BuildTab, LogKind, Mode, State}; |
| 8 | use crate::theme::{ACCENT, HERO, palette}; |
| 9 | use crate::widgets; |
| 10 | |
| 11 | pub fn show(ui: &mut Ui, st: &mut State) { |
| 12 | let p = palette(ui.ctx()); |
| 13 | let summary = st.doc().map(|d| match &d.session.last_asm { |
| 14 | Some((src, out)) if out.ok() => { |
| 15 | (out.code_size().to_string(), "bytes", false, src != &d.text) |
| 16 | } |
| 17 | Some((src, out)) => ( |
| 18 | out.diagnostics.len().to_string(), |
| 19 | "errors", |
| 20 | true, |
| 21 | src != &d.text, |
| 22 | ), |
| 23 | None => (String::new(), "not built", false, false), |
| 24 | }); |
| 25 | ui.horizontal(|ui| { |
| 26 | if let Some((n, unit, err, changed)) = &summary { |
| 27 | let color = if *err { ACCENT } else { p.text_display }; |
| 28 | // The PC is the hero while debugging; one per screen. |
| 29 | if n.is_empty() { |
| 30 | } else if st.mode == Mode::Edit { |
| 31 | ui.label(p.hero(n, HERO).color(color)); |
| 32 | } else { |
| 33 | ui.label(p.data(n).size(18.0).color(color)); |
| 34 | } |
| 35 | ui.vertical(|ui| { |
| 36 | ui.add_space(if st.mode == Mode::Edit { 14.0 } else { 2.0 }); |
| 37 | ui.label(p.label(*unit)); |
| 38 | if *changed { |
| 39 | ui.label(p.label("[source changed]").color(p.text_disabled)); |
| 40 | } |
| 41 | }); |
| 42 | } |
| 43 | ui.with_layout(Layout::right_to_left(Align::Min), |ui| { |
| 44 | widgets::segmented( |
| 45 | ui, |
| 46 | &mut st.build_tab, |
| 47 | &[ |
| 48 | (BuildTab::Messages, "Messages"), |
| 49 | (BuildTab::Listing, "Listing"), |
| 50 | (BuildTab::Hex, "HEX"), |
| 51 | ], |
| 52 | ); |
| 53 | }); |
| 54 | }); |
| 55 | ui.add_space(8.0); |
| 56 | match st.build_tab { |
| 57 | BuildTab::Messages => messages(ui, st), |
| 58 | BuildTab::Listing => listing(ui, st), |
| 59 | BuildTab::Hex => hex(ui, st), |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | fn messages(ui: &mut Ui, st: &mut State) { |
| 64 | let p = palette(ui.ctx()); |
| 65 | if st.log.is_empty() { |
| 66 | widgets::empty_state( |
| 67 | ui, |
| 68 | "No messages", |
| 69 | "Build output appears here. F7 assembles.", |
| 70 | ); |
| 71 | return; |
| 72 | } |
| 73 | let mut goto = None; |
| 74 | egui::ScrollArea::vertical() |
| 75 | .stick_to_bottom(true) |
| 76 | .auto_shrink([false; 2]) |
| 77 | .show(ui, |ui| { |
| 78 | ui.spacing_mut().item_spacing.y = 3.0; |
| 79 | for line in &st.log { |
| 80 | let color = match line.kind { |
| 81 | LogKind::Info => p.text_secondary, |
| 82 | LogKind::Ok => p.text_primary, |
| 83 | LogKind::Error => ACCENT, |
| 84 | }; |
| 85 | let text = RichText::new(&line.text) |
| 86 | .font(FontId::monospace(12.5)) |
| 87 | .color(color); |
| 88 | if let Some(target) = line.target { |
| 89 | let r = ui |
| 90 | .add(egui::Label::new(text).sense(Sense::click())) |
| 91 | .on_hover_cursor(CursorIcon::PointingHand); |
| 92 | if r.clicked() { |
| 93 | goto = Some(target); |
| 94 | } |
| 95 | } else { |
| 96 | ui.label(text); |
| 97 | } |
| 98 | } |
| 99 | }); |
| 100 | if let Some((doc, line, col)) = goto { |
| 101 | st.actions.push(Action::Goto { doc, line, col }); |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | fn rows(ui: &mut Ui, id: &str, lines: &[&str], mut row: impl FnMut(&str) -> LayoutJob) { |
| 106 | let row_h = ui.fonts_mut(|f| f.row_height(&FontId::monospace(12.5))) + 2.0; |
| 107 | egui::ScrollArea::both() |
| 108 | .id_salt(id) |
| 109 | .auto_shrink([false; 2]) |
| 110 | .show_rows(ui, row_h, lines.len(), |ui, range| { |
| 111 | ui.spacing_mut().item_spacing.y = 0.0; |
| 112 | for i in range { |
| 113 | ui.add(egui::Label::new(row(lines[i])).extend()); |
| 114 | } |
| 115 | }); |
| 116 | } |
| 117 | |
| 118 | fn listing(ui: &mut Ui, st: &State) { |
| 119 | let p = palette(ui.ctx()); |
| 120 | let Some((src, out)) = st.doc().and_then(|d| d.session.last_asm.as_ref()) else { |
| 121 | widgets::empty_state(ui, "No listing", "Press F7 to assemble."); |
| 122 | return; |
| 123 | }; |
| 124 | let text = out.listing(src); |
| 125 | let lines: Vec<&str> = text.lines().collect(); |
| 126 | let size = 12.5; |
| 127 | rows(ui, "listing", &lines, |l| { |
| 128 | let font = FontId::monospace(size); |
| 129 | if l.starts_with("***") { |
| 130 | return LayoutJob::simple_singleline(l.to_string(), font, ACCENT); |
| 131 | } |
| 132 | let is_code = l.len() > 25 && l.is_char_boundary(25); |
| 133 | if is_code && (l.as_bytes()[0].is_ascii_hexdigit() || l.starts_with(' ')) { |
| 134 | let (head, rest) = l.split_at(25); |
| 135 | let mut job = LayoutJob::default(); |
| 136 | let head_color = if l.starts_with(' ') { |
| 137 | p.text_disabled |
| 138 | } else { |
| 139 | p.text_secondary |
| 140 | }; |
| 141 | job.append(head, 0.0, TextFormat::simple(font, head_color)); |
| 142 | for (r, tok) in tokens(rest) { |
| 143 | let (color, _) = p.syntax(tok); |
| 144 | job.append( |
| 145 | &rest[r], |
| 146 | 0.0, |
| 147 | TextFormat::simple(FontId::monospace(size), color), |
| 148 | ); |
| 149 | } |
| 150 | return job; |
| 151 | } |
| 152 | LayoutJob::simple_singleline(l.to_string(), font, p.text_secondary) |
| 153 | }); |
| 154 | } |
| 155 | |
| 156 | fn hex(ui: &mut Ui, st: &State) { |
| 157 | let p = palette(ui.ctx()); |
| 158 | let Some((_, out)) = st.doc().and_then(|d| d.session.last_asm.as_ref()) else { |
| 159 | widgets::empty_state(ui, "No HEX file", "Press F7 to assemble."); |
| 160 | return; |
| 161 | }; |
| 162 | if !out.ok() { |
| 163 | widgets::empty_state(ui, "No HEX file", "Fix the errors to generate Intel HEX."); |
| 164 | return; |
| 165 | } |
| 166 | let text = out.intel_hex(); |
| 167 | let lines: Vec<&str> = text.lines().collect(); |
| 168 | rows(ui, "hex", &lines, |l| { |
| 169 | let font = FontId::monospace(12.5); |
| 170 | let mut job = LayoutJob::default(); |
| 171 | if l.len() < 11 { |
| 172 | job.append(l, 0.0, TextFormat::simple(font, p.text_primary)); |
| 173 | return job; |
| 174 | } |
| 175 | let n = l.len(); |
| 176 | for (part, color) in [ |
| 177 | (&l[..1], p.text_disabled), |
| 178 | (&l[1..3], p.text_secondary), |
| 179 | (&l[3..7], p.text_display), |
| 180 | (&l[7..9], p.text_secondary), |
| 181 | (&l[9..n - 2], p.text_primary), |
| 182 | (&l[n - 2..], p.text_disabled), |
| 183 | ] { |
| 184 | job.append(part, 0.0, TextFormat::simple(font.clone(), color)); |
| 185 | } |
| 186 | job |
| 187 | }); |
| 188 | } |