commit 3c6d9b65a4d9a84522efc08d81af8db47612444c
parent 8a5c28ed87ba4af5254a874edae43ce8cd6750a2
Author: William Casarin <jb55@jb55.com>
Date: Sun, 15 Feb 2026 09:52:34 -0800
dave: fix inline code baseline alignment and add accent color
Use egui LayoutJob to combine text and code spans into a single
galley instead of separate ui.label() calls, fixing baseline
misalignment between monospace and proportional fonts. Add muted
amber accent color for inline code text and a subtle darker
background distinct from code blocks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
1 file changed, 78 insertions(+), 20 deletions(-)
diff --git a/crates/notedeck_dave/src/ui/markdown_ui.rs b/crates/notedeck_dave/src/ui/markdown_ui.rs
@@ -1,6 +1,7 @@
//! Markdown rendering for assistant messages using egui.
-use egui::{Color32, RichText, Ui};
+use egui::text::LayoutJob;
+use egui::{Color32, FontFamily, FontId, RichText, TextFormat, Ui};
use md_stream::{
parse_inline, CodeBlock, InlineElement, InlineStyle, ListItem, MdElement, Partial, PartialKind,
};
@@ -9,6 +10,7 @@ use md_stream::{
pub struct MdTheme {
pub heading_sizes: [f32; 6],
pub code_bg: Color32,
+ pub inline_code_bg: Color32,
pub code_text: Color32,
pub link_color: Color32,
pub blockquote_border: Color32,
@@ -17,10 +19,18 @@ pub struct MdTheme {
impl MdTheme {
pub fn from_visuals(visuals: &egui::Visuals) -> Self {
+ let bg = visuals.panel_fill;
+ // Inline code bg: slightly lighter than panel background
+ let inline_code_bg = Color32::from_rgb(
+ bg.r().saturating_add(15),
+ bg.g().saturating_add(15),
+ bg.b().saturating_add(15),
+ );
Self {
heading_sizes: [24.0, 20.0, 18.0, 16.0, 14.0, 12.0],
code_bg: visuals.extreme_bg_color,
- code_text: visuals.text_color(),
+ inline_code_bg,
+ code_text: Color32::from_rgb(0xD4, 0xA5, 0x74), // Muted amber/sand
link_color: Color32::from_rgb(100, 149, 237), // Cornflower blue
blockquote_border: visuals.widgets.noninteractive.bg_stroke.color,
blockquote_bg: visuals.faint_bg_color,
@@ -102,45 +112,93 @@ fn render_element(element: &MdElement, theme: &MdTheme, ui: &mut Ui) {
}
}
+/// Flush a LayoutJob as a wrapped label if it has any content.
+fn flush_job(job: &mut LayoutJob, ui: &mut Ui) {
+ if !job.text.is_empty() {
+ job.wrap.max_width = ui.available_width();
+ ui.add(egui::Label::new(std::mem::take(job)).wrap());
+ }
+}
+
fn render_inlines(inlines: &[InlineElement], theme: &MdTheme, ui: &mut Ui) {
+ let font_size = ui.style().text_styles[&egui::TextStyle::Body].size;
+ let text_color = ui.visuals().text_color();
+
+ let text_fmt = TextFormat {
+ font_id: FontId::new(font_size, FontFamily::Proportional),
+ color: text_color,
+ ..Default::default()
+ };
+
+ let code_fmt = TextFormat {
+ font_id: FontId::new(font_size, FontFamily::Monospace),
+ color: theme.code_text,
+ background: theme.inline_code_bg,
+ ..Default::default()
+ };
+
+ let italic_fmt = TextFormat {
+ font_id: FontId::new(font_size, FontFamily::Proportional),
+ color: text_color,
+ italics: true,
+ ..Default::default()
+ };
+
+ let strikethrough_fmt = TextFormat {
+ font_id: FontId::new(font_size, FontFamily::Proportional),
+ color: text_color,
+ strikethrough: egui::Stroke::new(1.0, text_color),
+ ..Default::default()
+ };
+
+ let mut job = LayoutJob::default();
+
for inline in inlines {
match inline {
InlineElement::Text(text) => {
- ui.label(text);
- }
-
- InlineElement::Styled { style, content } => {
- let rt = match style {
- InlineStyle::Bold => RichText::new(content).strong(),
- InlineStyle::Italic => RichText::new(content).italics(),
- InlineStyle::BoldItalic => RichText::new(content).strong().italics(),
- InlineStyle::Strikethrough => RichText::new(content).strikethrough(),
- };
- ui.label(rt);
+ job.append(text, 0.0, text_fmt.clone());
}
InlineElement::Code(code) => {
- ui.label(
- RichText::new(code)
- .monospace()
- .background_color(theme.code_bg),
- );
+ job.append(code, 0.0, code_fmt.clone());
}
+ InlineElement::Styled { style, content } => match style {
+ InlineStyle::Italic => {
+ job.append(content, 0.0, italic_fmt.clone());
+ }
+ InlineStyle::Strikethrough => {
+ job.append(content, 0.0, strikethrough_fmt.clone());
+ }
+ InlineStyle::Bold | InlineStyle::BoldItalic => {
+ // TextFormat has no bold/weight — flush and render as separate label
+ flush_job(&mut job, ui);
+ let rt = if matches!(style, InlineStyle::BoldItalic) {
+ RichText::new(content).strong().italics()
+ } else {
+ RichText::new(content).strong()
+ };
+ ui.label(rt);
+ }
+ },
+
InlineElement::Link { text, url } => {
+ flush_job(&mut job, ui);
ui.hyperlink_to(RichText::new(text).color(theme.link_color), url);
}
InlineElement::Image { alt, url } => {
- // Render as link for now; full image support can be added later
+ flush_job(&mut job, ui);
ui.hyperlink_to(format!("[Image: {}]", alt), url);
}
InlineElement::LineBreak => {
- ui.end_row();
+ job.append("\n", 0.0, text_fmt.clone());
}
}
}
+
+ flush_job(&mut job, ui);
}
fn render_code_block(language: Option<&str>, content: &str, theme: &MdTheme, ui: &mut Ui) {