mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 20:02:10 +00:00
M6: a drop-down shows its arrow, and its list looks like the field
This commit is contained in:
@@ -13,7 +13,6 @@ use pdfcraft_engine::{Edit, FieldValue, FormField, FormFieldKind, FormWidget, fi
|
||||
use pdfcraft_render::DocInfo;
|
||||
|
||||
use crate::canvas::{DocView, PageXform};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
const FOCUS_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6);
|
||||
/// Background of the in-place text field editor, the same in every theme.
|
||||
@@ -363,10 +362,27 @@ pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, xf: &PageXform,
|
||||
} else if fillable(f) && pointer.is_some_and(|p| r.contains(p)) {
|
||||
painter.rect_stroke(r, CornerRadius::same(1), Stroke::new(1.0, FOCUS_BLUE.gamma_multiply(0.8)), egui::StrokeKind::Outside);
|
||||
}
|
||||
if f.kind == FormFieldKind::Combo && fillable(f) {
|
||||
paint_drop_down_arrow(painter, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A drop-down's arrow at the right of its box, which says the field opens a list (its page
|
||||
/// appearance shows only the value). Drawn on screen only, not into the file.
|
||||
fn paint_drop_down_arrow(painter: &egui::Painter, r: Rect) {
|
||||
let width = (r.height() * 0.45).clamp(5.0, 12.0);
|
||||
let inset = (r.height() * 0.25).clamp(2.0, 6.0);
|
||||
// Too narrow a box keeps its value visible instead.
|
||||
if r.width() < width * 3.0 {
|
||||
return;
|
||||
}
|
||||
let (x, y, half) = (r.max.x - inset - width / 2.0, r.center().y, width / 2.0);
|
||||
let points = vec![egui::pos2(x - half, y - half / 2.0), egui::pos2(x + half, y - half / 2.0), egui::pos2(x, y + half / 2.0)];
|
||||
painter.add(egui::Shape::convex_polygon(points, Color32::BLACK, Stroke::NONE));
|
||||
}
|
||||
|
||||
/// The size, in points, a text field shows `text` at on screen: its `/DA` size, or for auto size
|
||||
/// (`0 Tf`) the page appearance's fit, measured in the font the screen draws with so the text
|
||||
/// stays inside the box (the interface font runs wider than the page's Helvetica).
|
||||
@@ -490,7 +506,6 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
|
||||
let xf = view.page_xform(page)?;
|
||||
let rect = widget_rect(&xf, info, page, w.rect);
|
||||
let zoom = xf.rect.width() / xf.pw.max(1.0);
|
||||
let t = Tokens::get(ctx);
|
||||
let mut edit = None;
|
||||
let mut close = false;
|
||||
let mut next: Option<bool> = None; // Tab / Shift+Tab
|
||||
@@ -581,24 +596,47 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
|
||||
}
|
||||
}
|
||||
FormFieldKind::Combo => {
|
||||
let resp = egui::Area::new(egui::Id::new(("form-choices", view.id.0)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(rect.left_bottom() + vec2(0.0, 2.0))
|
||||
.show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(rect.width().max(160.0));
|
||||
ui.label(egui::RichText::new(&f.name).small().color(t.text_faint));
|
||||
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
|
||||
// The list opens under the field and looks like it, as other viewers draw it: as wide as
|
||||
// the box, in its text size, black on white, without the field's internal name.
|
||||
let size = (field_text_size(&ctx.layer_painter(egui::LayerId::background()), f, w, "") * zoom).clamp(6.0, 48.0);
|
||||
let row = (size * 1.3).ceil();
|
||||
let resp = egui::Area::new(egui::Id::new(("form-choices", view.id.0))).order(egui::Order::Foreground).fixed_pos(rect.left_bottom()).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
ui.style_mut().visuals = egui::Visuals::light();
|
||||
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
|
||||
egui::Frame::new().fill(Color32::WHITE).stroke(Stroke::new(1.0, Color32::from_gray(64))).show(ui, |ui| {
|
||||
let width = rect.width().max(24.0);
|
||||
ui.set_width(width);
|
||||
egui::ScrollArea::vertical().max_height(row * 9.0).show(ui, |ui| {
|
||||
for (export, display) in &f.options {
|
||||
let on = f.value.first() == Some(export);
|
||||
if ui.selectable_label(on, display).clicked() {
|
||||
let (r, item) = ui.allocate_exact_size(vec2(ui.available_width(), row), egui::Sense::click());
|
||||
item.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, on, display));
|
||||
let (fill, ink) = if on {
|
||||
(Color32::from_rgb(0x2F, 0x6F, 0xE0), Color32::WHITE)
|
||||
} else if item.hovered() {
|
||||
(Color32::from_gray(222), Color32::BLACK)
|
||||
} else {
|
||||
(Color32::WHITE, Color32::BLACK)
|
||||
};
|
||||
ui.painter().rect_filled(r, CornerRadius::ZERO, fill);
|
||||
ui.painter().with_clip_rect(r).text(
|
||||
egui::pos2(r.min.x + 3.0, r.center().y),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
display,
|
||||
egui::FontId::proportional(size),
|
||||
ink,
|
||||
);
|
||||
if item.clicked() {
|
||||
edit = Some(Edit::SetFieldValue { name: f.name.clone(), value: FieldValue::Choice(vec![export.clone()]) });
|
||||
close = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
let clicked_away = ctx.input(|i| i.pointer.any_click())
|
||||
&& !resp.response.contains_pointer()
|
||||
&& !ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| rect.contains(p));
|
||||
|
||||
@@ -1072,3 +1072,53 @@ fn auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box() {
|
||||
// An explicit size is kept, as in Acrobat: the text may then run past the box.
|
||||
assert_eq!(on_screen_fit(&h, "fixed", long).0, 10.0);
|
||||
}
|
||||
|
||||
fn field_rect(h: &Harness<'static, PdfCraftApp>, name: &str) -> egui::Rect {
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
let f = doc.form.iter().find(|f| f.name == name).unwrap();
|
||||
pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, 0).expect("on screen")
|
||||
}
|
||||
|
||||
/// Filled triangles painted inside `within`, at any depth of the frame's shapes.
|
||||
fn triangles_in(h: &Harness<'static, PdfCraftApp>, within: egui::Rect) -> Vec<Vec<egui::Pos2>> {
|
||||
fn walk(shape: &egui::Shape, within: egui::Rect, out: &mut Vec<Vec<egui::Pos2>>) {
|
||||
match shape {
|
||||
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| walk(s, within, out)),
|
||||
egui::Shape::Path(p) if p.points.len() == 3 && p.fill != egui::Color32::TRANSPARENT && p.points.iter().all(|q| within.contains(*q)) => {
|
||||
out.push(p.points.clone())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for c in &h.output().shapes {
|
||||
walk(&c.shape, within, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_down_shows_its_arrow_and_text_fields_do_not() {
|
||||
let mut h = harness();
|
||||
h.run_steps(2);
|
||||
let country = field_rect(&h, "country");
|
||||
let arrows = triangles_in(&h, country);
|
||||
assert_eq!(arrows.len(), 1, "one arrow in the drop-down: {arrows:?}");
|
||||
let tip = arrows[0].iter().fold(egui::Pos2::ZERO, |a, p| egui::pos2(a.x.max(p.x), a.y.max(p.y)));
|
||||
assert!(tip.x > country.center().x, "at the right of the box");
|
||||
assert!(triangles_in(&h, field_rect(&h, "city")).is_empty(), "a text field has none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drop_downs_list_opens_as_wide_as_the_field_without_its_internal_name() {
|
||||
let mut h = harness();
|
||||
click_field(&mut h, "country", 0);
|
||||
let country = field_rect(&h, "country");
|
||||
assert!(egui_kittest::kittest::Queryable::query_by_label(&h, "country").is_none(), "the field's name isn't shown");
|
||||
let item = egui_kittest::kittest::Queryable::get_by_label(&h, "France").rect();
|
||||
assert!((item.left() - country.left()).abs() <= 2.0 && item.right() <= country.right() + 1.0, "{item:?} in {country:?}");
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "France").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(value(&h, "country"), ["fr"]);
|
||||
}
|
||||
|
||||
@@ -4440,8 +4440,8 @@ milestone = "M6"
|
||||
status = "shipped"
|
||||
commands = ["form.fields"]
|
||||
tools = ["form_fill"]
|
||||
evidence = ["crates/ui-egui/tests/forms.rs::check_boxes_radios_and_choices", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text"]
|
||||
notes = "combo and list boxes, multi-select lists; editable combo boxes accept free text"
|
||||
evidence = ["crates/ui-egui/tests/forms.rs::check_boxes_radios_and_choices", "crates/ui-egui/tests/forms.rs::a_drop_downs_list_opens_as_wide_as_the_field_without_its_internal_name", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text"]
|
||||
notes = "combo and list boxes, multi-select lists; editable combo boxes accept free text. A combo box shows its arrow on screen and opens its list under the field, as wide as the box and in its text size, black on white; the list takes no arrow keys yet"
|
||||
|
||||
[[feature]]
|
||||
id = "form.push-buttons"
|
||||
|
||||
Reference in New Issue
Block a user