fix text caret not showing for empty input (#9336)

Fixes #9331

Fixed issues with wrong initial size and missing edit caret in text widgets.

<img width="311" alt="image" src="https://github.com/enso-org/enso/assets/919491/44f257cc-18a1-4a9f-9ae0-c1dd9b86674e">

# Important Notes
Automated tests for font loading/initial size will follow shortly. The text caret is not really testable, since it is a hosted object visual issue.
This commit is contained in:
Paweł Grabarz 2024-03-08 19:03:23 +01:00 committed by GitHub
parent 2330fdb8af
commit cee795b5e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 44 additions and 199 deletions

View File

@ -71,7 +71,6 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
<style scoped>
.WidgetText {
display: inline-flex;
vertical-align: middle;
background: var(--color-widget);
border-radius: var(--radius-full);
position: relative;
@ -80,6 +79,7 @@ export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
padding: 0px 4px;
min-width: 24px;
justify-content: center;
align-items: center;
&:has(> .AutoSizedInput:focus) {
outline: none;

View File

@ -24,8 +24,11 @@ const cssFont = computed(() => {
return style.font
})
// Add some extra spacing to allow the text caret to show at the end of input.
const ADDED_WIDTH_PX = 2
const getTextWidth = (text: string) => getTextWidthByFont(text, cssFont.value)
const inputWidth = computed(() => getTextWidth(`${innerModel.value}`))
const inputWidth = computed(() => getTextWidth(`${innerModel.value}`) + ADDED_WIDTH_PX)
const inputStyle = computed<StyleValue>(() => ({ width: `${inputWidth.value}px` }))
function onEnterDown() {

View File

@ -16,11 +16,18 @@ export function getTextWidthBySizeAndFamily(
/** Helper function to get text width. `font` is a CSS font specification as per https://developer.mozilla.org/en-US/docs/Web/CSS/font. */
export function getTextWidthByFont(text: string | null | undefined, font: string) {
if (text == null || font == '' || !fontReady(font)) {
if (text == null || text == '' || font == '') {
return 0
}
// Continue with `measureText` even if font is not loaded yet. What matters here is tracking
// reactive state of font loading in case it isn't ready yet, so the width will be recomputed
// once the loading finishes.
const _ = fontReady(font)
const context = getMeasureContext()
context.font = font
context.fillText(text, 0, 0)
const metrics = context.measureText(text)
return metrics.width
}
@ -31,32 +38,44 @@ export function getTextWidthByFont(text: string | null | undefined, font: string
* revert back to loading (assuming we don't dynamically change existing @font-face definitions to
* point to different URLs, which would be incredibly cursed).
*/
const fontsReady = shallowReactive(new Map())
const fontsLoadState = shallowReactive(new Map())
/**
* Check if given font is ready to use. In case if it is not, the check will automatically register
* a reactive dependency, which will be notified once loading is complete.
* @param font
* @returns
*/
function fontReady(font: string): boolean {
const readyState = fontsReady.get(font)
const readyState = fontsLoadState.get(font)
if (readyState === undefined) {
let syncReady
try {
// This operation can fail if the provided font string is not a valid CSS font specifier.
syncReady = document.fonts.check(font)
} catch (e) {
console.error(e)
// In case of exception, treat the font as if it was loaded. That way we don't attempt loading
// it again, and the browser font fallback logic should still make things behave more or less
// correct.
syncReady = true
}
fontsReady.set(font, syncReady)
if (syncReady) return true
else document.fonts.load(font).then(() => fontsReady.set(font, true))
return false
// Make sure to schedule font loading in separate task, so there are no immediate side effects
// on reactive state when this function is used in computed context.
setTimeout(() => loadFont(font), 0)
// This check by itself is not reactive, but it is fine since already track the loading state
// and the loading will begin shortly.
return checkFontSync(font)
}
return readyState
}
/** Start loading given font if it wasn't started yet. Mutates reactive state. */
function loadFont(font: string): void {
if (!fontsLoadState.has(font)) {
const ready = checkFontSync(font)
fontsLoadState.set(font, ready)
if (!ready) document.fonts.load(font).then(() => fontsLoadState.set(font, true))
}
}
/** Check if given font is loaded. NOT reactive. */
function checkFontSync(font: string): boolean {
try {
// This operation can fail if the provided font string is not a valid CSS font specifier.
return document.fonts.check(font)
} catch (e) {
console.error(e)
// In case of exception, treat the font as if it was loaded. That way we don't attempt loading
// it again, and the browser font fallback logic should still make things behave more or less
// correct.
return true
}
}

View File

@ -1,177 +0,0 @@
//! Generation of Dim* macros, macros allowing generation of swizzling getters and setters.
// === Non-Standard Linter Configuration ===
#![allow(clippy::bool_to_int_with_if)]
#![allow(clippy::let_and_return)]
#![allow(clippy::option_map_unit_fn)]
#![allow(clippy::precedence)]
#![allow(dead_code)]
#![deny(non_ascii_idents)]
#![deny(unconditional_recursion)]
#![warn(unsafe_code)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#![warn(missing_docs)]
#![warn(trivial_casts)]
#![warn(trivial_numeric_casts)]
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
use enso_prelude::*;
use std::fmt::Write;
use std::fs::File;
use std::io::Write as IoWrite;
// =================
// === Constants ===
// =================
const FILE: &str = "src/dim_macros.rs";
const AXES: &[&str] = &["x", "y", "z", "w"];
const INDENT_SIZE: usize = 4;
// ========================
// === Formatting Utils ===
// ========================
fn indent(level: usize) -> String {
" ".repeat(level * INDENT_SIZE)
}
// ======================
// === Implementation ===
// ======================
/// Generates swizzling data. The [`base_dim`] describes what dimension the data should be generated
/// in. For example, if it is set to 3, all of "x", "y", and "z" axes will be combined to generate
/// the result. The [`dim`] describes the dimension of the swizzling. For example, if it is set to
/// 2, the result will contain 2-dimensional coordinates, like "xy", or "yz". If the [`unique`] flag
/// is set, the generated swizzling will not have repeated axes (e.g. `xx` is not allowed).
///
/// The output is a vector of four elements:
/// - The swizzling name, like "xy", "xyz", "xz", etc.
/// - The swizzling dimension.
/// - The swizzling component indexes. For example [0, 2] for "xz".
/// - The enumeration of components. For example [0, 1] for "xz".
///
/// For example, for the base dimension of 3 and the dimension of 2, the following swizzles will be
/// generated:
///
/// ```text
/// xz 2 [0, 2] [0, 1]
/// yz 2 [1, 2] [0, 1]
/// zx 2 [2, 0] [0, 1]
/// zy 2 [2, 1] [0, 1]
/// zz 2 [2, 2] [0, 1]
/// ```
///
/// See the generated [`FILE`] to see the result of this script.
fn gen_swizzling(
base_dim: usize,
dim: usize,
unique: bool,
) -> Vec<(Vec<String>, usize, Vec<usize>, Vec<usize>)> {
let mut vec: Vec<(Vec<String>, Vec<usize>, Vec<usize>)> = vec![(vec![], vec![], vec![])];
for _ in 0..dim {
vec = vec
.clone()
.into_iter()
.cartesian_product(AXES[0..base_dim].iter().enumerate())
.filter_map(|((mut prod, mut ixs, mut ord), (ix, axis))| {
if unique && ixs.contains(&ix) {
return None;
}
prod.push(axis.to_string());
ixs.push(ix);
ord.push(ord.len());
Some((prod, ixs, ord))
})
.collect_vec();
}
vec.into_iter().map(|(prod, ixs, ord)| (prod, dim, ixs, ord)).collect_vec()
}
/// Just like [`gen_swizzling`], but the output always contains the dimension component. For
/// example, if the dimension was set to 2, the output will contain all swizzling combinations that
/// contain the "z" component.
fn gen_swizzling_force_dim_component(
input_dim: usize,
dim: usize,
unique: bool,
) -> Vec<(Vec<String>, usize, Vec<usize>, Vec<usize>)> {
let axe = AXES[input_dim - 1];
gen_swizzling(input_dim, dim, unique)
.into_iter()
.filter(|(axes, _, _, _)| axes.contains(&axe.to_string()))
.collect()
}
fn gen_swizzling_macro_branch(input_dim: usize, unique: bool) -> String {
let mut out = String::new();
out.write_str(&format!(
"{}({}, $f: ident $(,$($args:tt)*)?) => {{ $f! {{ $([$($args)*])? {}\n",
indent(1),
input_dim,
input_dim,
))
.unwrap();
for dim in 1..input_dim {
for (axes, dim, ixs, ord) in gen_swizzling_force_dim_component(input_dim, dim, unique) {
out.write_str(&format!("{}{} {} {:?} {:?}\n", indent(2), axes.join(""), dim, ixs, ord))
.unwrap();
}
}
for (axes, dim, ixs, ord) in gen_swizzling(input_dim, input_dim, unique) {
out.write_str(&format!("{}{} {} {:?} {:?}\n", indent(2), axes.join(""), dim, ixs, ord))
.unwrap();
}
out.write_str(&format!("{}}}}};\n", indent(1))).unwrap();
out
}
/// The generated macro accepts two arguments, the dimension of the swizzling and another macro name
/// that should be called with the swizzling data. The provided macro will be called with the chosen
/// dimension and the data generated by the [`gen_swizzling_macro_branch`] function.
///
/// See the generated [`FILE`] to see the result of this script.
fn gen_swizzling_macro(unique: bool) -> String {
let mut out = String::new();
let sfx = if unique { "_unique" } else { "" };
out.write_str("/// Swizzling data for the given dimension.\n").unwrap();
out.write_str("/// See the [`build.rs`] file to learn more.\n").unwrap();
out.write_str("#[macro_export]\n").unwrap();
out.write_str(&format!("macro_rules! with_swizzling_for_dim{sfx} {{\n")).unwrap();
out.write_str(&gen_swizzling_macro_branch(1, unique)).unwrap();
out.write_str(&gen_swizzling_macro_branch(2, unique)).unwrap();
out.write_str(&gen_swizzling_macro_branch(3, unique)).unwrap();
out.write_str(&gen_swizzling_macro_branch(4, unique)).unwrap();
out.write_str("}").unwrap();
out
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let mut file = File::create(FILE).unwrap();
let warning = "THIS IS AN AUTO-GENERATED FILE. DO NOT EDIT IT DIRECTLY!";
let border = "!".repeat(warning.len());
let mut out = String::new();
out.write_str("//! Macros allowing generation of swizzling getters and setters.\n").unwrap();
out.write_str("//! See the docs of [`build.rs`] and usage places to learn more.\n").unwrap();
out.write_str(&format!("\n\n\n// {border}\n")).unwrap();
out.write_str("// THIS IS AN AUTO-GENERATED FILE. DO NOT EDIT IT DIRECTLY!\n").unwrap();
out.write_str(&format!("// {border}\n\n\n")).unwrap();
out.write_str(&gen_swizzling_macro(false)).unwrap();
out.write_str("\n\n").unwrap();
out.write_str(&gen_swizzling_macro(true)).unwrap();
out.write_str("\n").unwrap();
file.write_all(out.as_bytes()).unwrap();
}