mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-02 06:46:50 -06:00
Live font preview on hover in font picker (#653)
Hovering a family in the Typography font dropdown previews it on the canvas via text.preview; closing without picking restores the original, clicking applies it as one undo step.
This commit is contained in:
@@ -332,7 +332,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"text.preview",
|
||||
"Preview text change",
|
||||
"{command: style.apply|library.applyStyle|text.setVariations|text.style, params} shows what the command would do on the canvas without an undo step (a hovered text style or named instance, a dragged axis slider), in place of an earlier preview; text.endPreview or any other command ends it, so running the command for real commits it as one undo step",
|
||||
"{command: style.apply|library.applyStyle|text.setVariations|text.style, params} shows what the command would do on the canvas without an undo step (a hovered font family or style, text style or named instance, a dragged axis slider), in place of an earlier preview; text.endPreview or any other command ends it, so running the command for real commits it as one undo step",
|
||||
false,
|
||||
|s, p| {
|
||||
let c = req_string("text.preview", p, "command")?;
|
||||
|
||||
@@ -316,8 +316,8 @@ pub struct DocState {
|
||||
versions_changed: bool,
|
||||
/// A version shown in place of the document (read-only).
|
||||
pub preview: Option<VersionPreview>,
|
||||
/// The document under a live text preview (a hovered text style or font instance, a dragged
|
||||
/// axis slider): `doc` shows the preview until [`textpreview::end`] puts this back.
|
||||
/// The document under a live text preview (a hovered font family or style, text style or
|
||||
/// font instance, a dragged axis slider): `doc` shows the preview until [`textpreview::end`] puts this back.
|
||||
pub text_preview: Option<Arc<Document>>,
|
||||
/// The document as of the last version saved or restored (none yet): autosaves, restores and
|
||||
/// merges only add a checkpoint for changes since.
|
||||
|
||||
@@ -212,6 +212,31 @@ fn previews_while_editing_keep_the_edit() {
|
||||
assert!(links(&s, t).iter().all(Option::is_none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previews_show_a_hovered_font_family() {
|
||||
let mut s = new_session();
|
||||
let t = text(&mut s, "Preview me");
|
||||
s.ds_mut().unwrap().mark_saved();
|
||||
let before = s.doc().unwrap().clone();
|
||||
let undo = s.ds().unwrap().history.undo.len();
|
||||
// Hovering a font family shows it on the canvas: no undo step, not dirty.
|
||||
s.execute("text.preview", &json!({"command": "text.style", "params": {"ids": [t], "fontFamily": "Arial"}})).unwrap();
|
||||
assert_eq!(data(&s, t).style_at(0).font_family, "Arial");
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo);
|
||||
assert!(!s.ds().unwrap().is_dirty());
|
||||
// Moving to another family replaces the preview, from the document itself.
|
||||
s.execute("text.preview", &json!({"command": "text.style", "params": {"ids": [t], "fontFamily": "Georgia"}})).unwrap();
|
||||
assert_eq!(data(&s, t).style_at(0).font_family, "Georgia");
|
||||
// Closing without picking puts the document back as it was.
|
||||
assert_eq!(s.execute("text.endPreview", &json!({})).unwrap()["ended"], json!(true));
|
||||
assert_eq!(*s.doc().unwrap(), before);
|
||||
// Clicking commits it as one undo step.
|
||||
s.execute("text.style", &json!({"ids": [t], "fontFamily": "Georgia"})).unwrap();
|
||||
assert_eq!(data(&s, t).style_at(0).font_family, "Georgia");
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo + 1);
|
||||
assert!(s.ds().unwrap().text_preview.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previews_refuse_what_they_cant_show() {
|
||||
let mut s = new_session();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Live text previews: what a text command would do, shown on the canvas without an undo step —
|
||||
//! while the pointer is over a text style in the style browser or a font's named instance, or
|
||||
//! while a variable font axis slider is dragged. The document under the preview is kept and comes
|
||||
//! while the pointer is over a font family or style, a text style in the style browser or a
|
||||
//! font's named instance, or while a variable font axis slider is dragged. The document under the preview is kept and comes
|
||||
//! back when the preview ends ([`end`], `text.endPreview`). Every other command ends it first, so
|
||||
//! committing (running the command for real) applies to the document itself as one undo step, and
|
||||
//! Escape just ends the preview.
|
||||
|
||||
@@ -583,7 +583,7 @@ impl FigApp {
|
||||
}
|
||||
|
||||
/// Show what `command params` would do on the canvas as a live preview (`text.preview`) while
|
||||
/// a text style, a named instance or an axis slider is hovered or dragged. Call it every frame
|
||||
/// a font family, font style, text style, named instance or axis slider is hovered or dragged. Call it every frame
|
||||
/// the preview should stay: it ends on the first frame nothing asks for it.
|
||||
pub fn preview_text(&mut self, ctx: &egui::Context, command: &str, params: Value) {
|
||||
self.ui.text_preview_wanted = true;
|
||||
|
||||
@@ -746,7 +746,7 @@ fn text_properties(ui: &mut Ui, id: &str, def: &TextStyle, out: &mut Frame) {
|
||||
(family_resp, style_resp)
|
||||
});
|
||||
crate::right::menu_popup(&family_resp).width(260.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if let Some(f) = crate::typography::font_picker(ui, Some(&def.font_family), family_resp.clicked()) {
|
||||
if let Some(f) = crate::typography::font_picker(ui, Some(&def.font_family), family_resp.clicked()).0 {
|
||||
set(out, &|d| {
|
||||
d.font_family = f.clone();
|
||||
let styles = figcraft_text::family_styles(&f);
|
||||
|
||||
@@ -181,7 +181,12 @@ fn font_rows(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], styles: &[TextStyle]
|
||||
let w = ui.available_width();
|
||||
let resp = widgets::dropdown(ui, family.as_deref().unwrap_or(tr(MIXED)), w, None);
|
||||
menu_popup(&resp).width(w.max(240.0)).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if let Some(f) = font_picker(ui, family.as_deref(), resp.clicked()) {
|
||||
let (picked, hovered) = font_picker(ui, family.as_deref(), resp.clicked());
|
||||
if let Some(h) = hovered {
|
||||
// The canvas shows the hovered family; closing without picking ends the preview.
|
||||
app.preview_text(ui.ctx(), "text.style", text_style_params(ids, range, json!({"fontFamily": h})));
|
||||
}
|
||||
if let Some(f) = picked {
|
||||
remember_family(ui.ctx(), &f);
|
||||
apply_style(app, "fontFamily", json!(f));
|
||||
ui.close();
|
||||
@@ -1049,8 +1054,9 @@ enum PickRow {
|
||||
Family(String),
|
||||
}
|
||||
|
||||
/// The searchable family list. Returns the family picked.
|
||||
pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool) -> Option<String> {
|
||||
/// The searchable family list. Returns the family picked and the family hovered (the caller
|
||||
/// previews the hovered one on the canvas: closing without picking restores the original).
|
||||
pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool) -> (Option<String>, Option<String>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let q_id = Id::new("font_query");
|
||||
let mut q: String = ui.data(|d| d.get_temp(q_id)).unwrap_or_default();
|
||||
@@ -1066,7 +1072,7 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
|
||||
let all = figcraft_text::family_names();
|
||||
let matches: Vec<String> = all.iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)).cloned().collect();
|
||||
if search.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
return matches.into_iter().next();
|
||||
return (matches.into_iter().next(), None);
|
||||
}
|
||||
let mut rows: Vec<PickRow> = Vec::new();
|
||||
if ql.is_empty() {
|
||||
@@ -1079,11 +1085,12 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
|
||||
}
|
||||
if matches.is_empty() {
|
||||
widgets::label(ui, tr("No fonts match"), t.text_tertiary);
|
||||
return None;
|
||||
return (None, None);
|
||||
}
|
||||
rows.extend(matches.into_iter().map(PickRow::Family));
|
||||
let mut budget = 4;
|
||||
let mut picked = None;
|
||||
let mut hovered_family = None;
|
||||
egui::ScrollArea::vertical().max_height(380.0).auto_shrink([false, true]).show_rows(ui, ROW, rows.len(), |ui, range| {
|
||||
for r in rows.get(range).unwrap_or(&[]) {
|
||||
let w = ui.available_width();
|
||||
@@ -1125,11 +1132,14 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
|
||||
if resp.clicked() {
|
||||
picked = Some(f.clone());
|
||||
}
|
||||
if resp.hovered() {
|
||||
hovered_family = Some(f.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
picked
|
||||
(picked, hovered_family)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user