Files
Marcos Paulo 5e8020aa38
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Obsidian basics, editor previews, graph rework, query blocks, stacked tabs, pop-out windows
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace
with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/
video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked
tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits,
block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 11:21:19 -03:00

203 lines
8.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Headless geometry and interaction contracts for sidebar pills and unified diffs.
use egui::{Context, Event, RawInput, Rect, Shape, pos2, vec2};
use notescraft_git::{Diff, DiffLine, Hunk, LineKind};
use notescraft_ui_egui::{
diff_view, fonts, motion, sidebar_tabs,
theme::{Tokens, color},
};
fn setup() -> Option<(Context, Tokens)> {
let tokens = Tokens::new(notescraft_themes::builtin("catppuccin-mocha")?);
let ctx = Context::default();
fonts::install(&ctx);
egui_extras::install_image_loaders(&ctx);
motion::set_enabled(&ctx, false);
let mut warm = ctx.run_ui(RawInput::default(), |_| {});
warm.textures_delta.clear();
Some((ctx, tokens))
}
fn frame(ctx: &Context, width: f32, events: Vec<Event>, mut draw: impl FnMut(&mut egui::Ui)) -> egui::FullOutput {
let mut output = ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(width, 900.0))), events, ..Default::default() }, |ui| {
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
ui.set_width(width - 16.0);
draw(ui);
});
output.textures_delta.clear();
output
}
fn click(point: egui::Pos2) -> Vec<Event> {
vec![
Event::PointerMoved(point),
Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE },
Event::PointerButton { pos: point, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE },
]
}
#[test]
fn tab_pills_have_fixed_size_radius_and_capped_count_badge() {
let Some((ctx, tokens)) = setup() else { return };
let mut tab = sidebar_tabs::SidebarTab::Search;
let output = frame(&ctx, 268.0, Vec::new(), |ui| sidebar_tabs::draw(ui, &tokens, &mut tab, 125));
let pill = output.shapes.iter().find_map(|shape| match &shape.shape {
Shape::Rect(rect) if rect.fill == color(tokens.theme.colors.surface_2) => Some(rect),
_ => None,
});
assert!(pill.is_some());
let Some(pill) = pill else { return };
// The pill is a whole cell of the strip: five equal cells that fill its width.
assert_eq!(pill.rect.height(), 36.0);
assert!(pill.rect.width() >= 40.0 && pill.rect.width() < 268.0 / 4.0, "{:?}", pill.rect.size());
assert_eq!(pill.corner_radius, egui::CornerRadius::same(12));
let badge = output.shapes.iter().find_map(|shape| match &shape.shape {
Shape::Rect(rect) if rect.fill == color(tokens.theme.colors.accent) => Some(rect),
_ => None,
});
assert!(badge.is_some());
let Some(badge) = badge else { return };
assert!(badge.rect.width() >= 16.0 && (badge.rect.height() - 16.0).abs() < 0.01);
assert!(output.shapes.iter().any(|shape| matches!(&shape.shape, Shape::Text(text) if text.galley.text() == "99+")));
}
#[test]
fn last_tab_aligns_to_strip_edge_and_changes_selection() {
let Some((ctx, tokens)) = setup() else { return };
let mut tab = sidebar_tabs::SidebarTab::Explorer;
let mut origin = egui::Pos2::ZERO;
let _ = frame(&ctx, 268.0, Vec::new(), |ui| {
origin = ui.cursor().min;
sidebar_tabs::draw(ui, &tokens, &mut tab, 0);
});
let _ = frame(&ctx, 268.0, click(origin + vec2(232.0, 18.0)), |ui| sidebar_tabs::draw(ui, &tokens, &mut tab, 0));
assert_eq!(tab, sidebar_tabs::SidebarTab::Source);
let output = frame(&ctx, 268.0, Vec::new(), |ui| sidebar_tabs::draw(ui, &tokens, &mut tab, 0));
assert!(output.shapes.iter().any(|shape| matches!(&shape.shape, Shape::Rect(rect) if rect.fill == color(tokens.theme.colors.surface_2) && (rect.rect.right() - origin.x - 252.0).abs() < 0.01)));
}
fn selection() -> diff_view::DiffSelection {
diff_view::DiffSelection {
path: "Projects/spec.md".into(),
staged: true,
revision: None,
review: false,
diff: Diff {
hunks: vec![Hunk {
header: "@@ -4,2 +4,2 @@".into(),
lines: vec![
DiffLine { kind: LineKind::Context, old_no: Some(4), new_no: Some(4), text: "context".into() },
DiffLine { kind: LineKind::Del, old_no: Some(5), new_no: None, text: "old code".into() },
DiffLine { kind: LineKind::Add, old_no: None, new_no: Some(5), text: "new code".into() },
],
}],
..Default::default()
},
}
}
#[test]
fn diff_uses_twenty_four_point_lines_and_separate_gutters() {
let Some((ctx, tokens)) = setup() else { return };
let selected = selection();
let output = frame(&ctx, 1100.0, Vec::new(), |ui| {
let _ = diff_view::draw(ui, &tokens, &selected);
});
let rows: Vec<_> = output
.shapes
.iter()
.filter_map(|shape| match &shape.shape {
Shape::Rect(rect)
if [color(tokens.theme.colors.success).gamma_multiply(0.13), color(tokens.theme.colors.danger).gamma_multiply(0.13)].contains(&rect.fill) =>
{
Some(rect.rect)
}
_ => None,
})
.collect();
assert_eq!(rows.len(), 2);
assert!(rows.iter().all(|rect| (rect.height() - 24.0).abs() < 0.01));
let text: Vec<_> = output
.shapes
.iter()
.filter_map(|shape| match &shape.shape {
Shape::Text(text) => Some(text),
_ => None,
})
.collect();
let old = text.iter().find(|text| text.galley.text() == "old code");
assert!(old.is_some());
let Some(old) = old else { return };
let number = text.iter().find(|text| text.galley.text() == "5" && (text.pos.y - old.pos.y).abs() < 5.0);
assert!(number.is_some());
let Some(number) = number else { return };
assert!(old.pos.x - (number.pos.x + number.galley.size().x) > 48.0);
assert!(text.iter().any(|text| text.galley.text() == "STAGED"));
assert!(text.iter().any(|text| text.galley.text() == "+1"));
assert!(text.iter().any(|text| text.galley.text() == "−1"));
}
#[test]
fn diff_back_button_has_card_geometry_and_binary_notice() {
let Some((ctx, tokens)) = setup() else { return };
let mut selected = selection();
selected.diff.binary = true;
let mut closed = false;
let output = frame(&ctx, 1100.0, Vec::new(), |ui| {
closed = diff_view::draw(ui, &tokens, &selected);
});
assert!(!closed);
let button = output.shapes.iter().find_map(|shape| match &shape.shape {
Shape::Rect(rect) if rect.rect.size() == vec2(36.0, 36.0) && rect.corner_radius == egui::CornerRadius::same(12) => Some(rect.rect),
_ => None,
});
assert!(button.is_some());
let Some(button) = button else { return };
let _ = frame(&ctx, 1100.0, click(button.center()), |ui| {
closed = diff_view::draw(ui, &tokens, &selected);
});
assert!(closed);
assert!(output.shapes.iter().any(|shape| matches!(&shape.shape, Shape::Text(text) if text.galley.text() == "Binary file")));
}
#[test]
fn oversized_diff_shows_limit_notice_before_scrolling() {
let Some((ctx, tokens)) = setup() else { return };
let mut selected = selection();
selected.diff.hunks = vec![Hunk {
header: "@@ -0,0 +1,2001 @@".into(),
lines: (0..=diff_view::MAX_VISIBLE_LINES).map(|_| DiffLine { kind: LineKind::Add, old_no: None, new_no: Some(1), text: "added".into() }).collect(),
}];
let output = frame(&ctx, 1100.0, Vec::new(), |ui| {
let _ = diff_view::draw(ui, &tokens, &selected);
});
let notice = output.shapes.iter().find_map(|shape| match &shape.shape {
Shape::Text(text) if text.galley.text().contains("Showing first 2 000") => Some(text.pos),
_ => None,
});
assert!(notice.is_some_and(|position| position.y < 120.0), "limit notice must remain visible above body");
}
#[test]
fn diff_copy_action_preserves_prefixes_and_long_lines_scroll() {
let Some((ctx, tokens)) = setup() else { return };
let mut selected = selection();
if let Some(hunk) = selected.diff.hunks.first_mut() {
hunk.lines.push(DiffLine { kind: LineKind::Add, old_no: None, new_no: Some(6), text: "long line ".repeat(100) });
}
let output = frame(&ctx, 1100.0, Vec::new(), |ui| {
let _ = diff_view::draw(ui, &tokens, &selected);
});
assert!(output.shapes.iter().any(|shape| matches!(&shape.shape, Shape::Rect(rect) if rect.rect.width() > 2000.0)));
let output = frame(&ctx, 1100.0, click(pos2(1048.0, 34.0)), |ui| {
let _ = diff_view::draw(ui, &tokens, &selected);
});
assert!(
output
.platform_output
.commands
.iter()
.any(|command| matches!(command, egui::OutputCommand::CopyText(text) if text.starts_with("@@ -4,2 +4,2 @@\n context\n-old code\n+new code\n")))
);
}