5e8020aa38
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>
203 lines
8.6 KiB
Rust
203 lines
8.6 KiB
Rust
//! 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")))
|
||
);
|
||
}
|