use crate::emacs_core::error::{EvalResult, signal};
use crate::emacs_core::intern::resolve_sym;
use crate::emacs_core::symbol::Obarray;
use crate::emacs_core::value::{HashKey, HashTableTest, Value, ValueKind, list_to_vec};
use crate::face::Face as RuntimeFace;
const FACE_ATTRIBUTES_VECTOR_LEN: usize = 20;
pub fn register_bootstrap_vars(obarray: &mut Obarray) {
obarray.set_symbol_value("face-filters-always-match", Value::NIL);
obarray.set_symbol_value(
"face--new-frame-defaults",
bootstrap_face_new_frame_defaults_table(),
);
obarray.set_symbol_value("face-default-stipple", Value::string("gray3"));
obarray.set_symbol_value("tty-defined-color-alist", Value::NIL);
obarray.set_symbol_value("scalable-fonts-allowed", Value::NIL);
obarray.set_symbol_value("face-ignored-fonts", Value::NIL);
obarray.set_symbol_value("face-remapping-alist", Value::NIL);
obarray.set_symbol_value("face-font-rescale-alist", Value::NIL);
obarray.set_symbol_value("face-near-same-color-threshold", Value::fixnum(30_000));
obarray.set_symbol_value("face-font-lax-matched-attributes", Value::T);
}
pub(crate) fn ensure_startup_compat_variables(eval: &mut crate::emacs_core::eval::Context) {
let defaults = [
("face-filters-always-match", Value::NIL),
(
"face--new-frame-defaults",
bootstrap_face_new_frame_defaults_table(),
),
("face-default-stipple", Value::string("gray3")),
("scalable-fonts-allowed", Value::NIL),
("face-ignored-fonts", Value::NIL),
("face-remapping-alist", Value::NIL),
("face-font-rescale-alist", Value::NIL),
("face-near-same-color-threshold", Value::fixnum(30_000)),
("face-font-lax-matched-attributes", Value::T),
];
for (name, value) in defaults {
if eval.obarray().symbol_value(name).is_none() {
eval.set_variable(name, value);
}
}
}
pub(crate) fn builtin_frame_face_hash_table(
eval: &mut crate::emacs_core::eval::Context,
args: Vec<Value>,
) -> EvalResult {
crate::emacs_core::display::expect_range_args("frame--face-hash-table", &args, 0, 1)?;
let frame_id = crate::emacs_core::window_cmds::resolve_frame_id_in_state(
&mut eval.frames,
&mut eval.buffers,
args.first(),
"frame-live-p",
)?;
Ok(eval
.frames
.get(frame_id)
.map(|frame| frame.face_hash_table())
.unwrap_or(Value::hash_table(HashTableTest::Eq)))
}
fn unspecified_face_attributes_vector() -> Value {
Value::vector(vec![
Value::symbol("unspecified");
FACE_ATTRIBUTES_VECTOR_LEN
])
}
fn face_attributes_vector_slot(attr_name: &str) -> Option<usize> {
match attr_name {
":family" => Some(1),
":foundry" => Some(2),
":width" => Some(3),
":height" => Some(4),
":weight" => Some(5),
":slant" => Some(6),
":underline" => Some(7),
":inverse-video" => Some(8),
":foreground" => Some(9),
":background" => Some(10),
":stipple" => Some(11),
":overline" => Some(12),
":strike-through" => Some(13),
":box" => Some(14),
":font" => Some(15),
":inherit" => Some(16),
":fontset" => Some(17),
":distant-foreground" => Some(18),
":extend" => Some(19),
_ => None,
}
}
fn face_attr_key_name(value: &Value) -> Option<&str> {
match value.kind() {
ValueKind::Symbol(id) => Some(resolve_sym(id)),
_ => None,
}
}
pub(crate) fn builtin_face_attributes_as_vector(args: Vec<Value>) -> EvalResult {
crate::emacs_core::display::expect_args("face-attributes-as-vector", &args, 1)?;
let mut attrs = vec![Value::symbol("unspecified"); FACE_ATTRIBUTES_VECTOR_LEN];
let Some(plist) = list_to_vec(&args[0]) else {
return Ok(Value::vector(attrs));
};
let mut i = 0;
while i + 1 < plist.len() {
let Some(attr_name) = face_attr_key_name(&plist[i]) else {
i += 2;
continue;
};
let Some(slot) = face_attributes_vector_slot(attr_name) else {
i += 2;
continue;
};
let value = plist[i + 1];
match attr_name {
":foreground" | ":background" | ":distant-foreground" if value.is_nil() => {}
":stipple" | ":font" | ":inherit" | ":fontset" => {}
":box" if value.is_t() => attrs[slot] = Value::fixnum(1),
_ => attrs[slot] = value,
}
i += 2;
}
Ok(Value::vector(attrs))
}
pub(crate) fn mirror_runtime_face_into_frame(
frame: &mut crate::window::Frame,
face_name: &str,
face: &RuntimeFace,
) {
frame.set_realized_face(face_name.to_string(), face.clone());
upsert_frame_face_hash_entry(
frame.face_hash_table(),
Value::symbol(face_name),
crate::emacs_core::font::runtime_face_to_lisp_vector(face),
);
}
pub(crate) fn seed_face_new_frame_defaults_table(table: Value) {
let face_names = crate::emacs_core::font::all_defined_face_names_sorted_by_id_desc();
let face_entries: Vec<(Value, Value)> = face_names
.into_iter()
.filter_map(|face_name| {
let face_id = crate::emacs_core::font::face_id_for_name(&face_name)?;
Some((
Value::symbol(face_name.as_str()),
Value::cons(
Value::fixnum(face_id),
crate::emacs_core::font::make_lisp_face_vector(),
),
))
})
.collect();
for (key, value) in face_entries {
upsert_frame_face_hash_entry(table, key, value);
}
}
fn bootstrap_face_new_frame_defaults_table() -> Value {
let table = Value::hash_table(HashTableTest::Eq);
seed_face_new_frame_defaults_table(table);
table
}
pub(crate) fn ensure_face_new_frame_defaults_entry(
eval: &mut crate::emacs_core::eval::Context,
face_name: &str,
) -> Option<Value> {
let table = eval
.obarray()
.symbol_value("face--new-frame-defaults")
.copied()?;
seed_face_new_frame_defaults_table(table);
let face_id = crate::emacs_core::font::face_id_for_name(face_name)?;
upsert_frame_face_hash_entry(
table,
Value::symbol(face_name),
Value::cons(
Value::fixnum(face_id),
crate::emacs_core::font::make_lisp_face_vector(),
),
);
Some(table)
}
fn upsert_frame_face_hash_entry(table: Value, key: Value, value: Value) {
if !table.is_hash_table() {
unreachable!("frame face hash table must be a hash table");
};
let _ = table.with_hash_table_mut(|hash_table| {
let hash_key = match key.kind() {
ValueKind::Symbol(id) => HashKey::Symbol(id),
_ => unreachable!("face hash keys are symbols"),
};
if !hash_table.data.contains_key(&hash_key) {
hash_table.insertion_order.push(hash_key.clone());
}
hash_table.key_snapshots.insert(hash_key.clone(), key);
hash_table.data.insert(hash_key, value);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_bootstrap_vars_matches_gnu_defaults() {
crate::test_utils::init_test_tracing();
let mut obarray = Obarray::new();
register_bootstrap_vars(&mut obarray);
assert_eq!(
obarray.symbol_value("face-default-stipple").copied(),
Some(Value::string("gray3"))
);
assert_eq!(
obarray
.symbol_value("face-near-same-color-threshold")
.copied(),
Some(Value::fixnum(30_000))
);
assert_eq!(
obarray
.symbol_value("face-font-lax-matched-attributes")
.copied(),
Some(Value::T)
);
let table = obarray
.symbol_value("face--new-frame-defaults")
.copied()
.expect("face--new-frame-defaults");
if !table.is_hash_table() {
panic!("face--new-frame-defaults must be a hash table");
};
let test = table.as_hash_table().unwrap().test.clone();
assert_eq!(test, HashTableTest::Eq);
}
#[test]
fn frame_face_hash_table_eval_is_empty_before_any_face_realization() {
crate::test_utils::init_test_tracing();
let mut eval = crate::emacs_core::eval::Context::new();
let out = builtin_frame_face_hash_table(&mut eval, vec![Value::NIL])
.expect("live frame face hash table");
if !out.is_hash_table() {
panic!("expected hash table");
};
let len = out.as_hash_table().unwrap().data.len();
assert_eq!(len, 0);
}
#[test]
fn frame_face_hash_table_eval_returns_stable_frame_owned_table() {
crate::test_utils::init_test_tracing();
let mut eval = crate::emacs_core::eval::Context::new();
let first = builtin_frame_face_hash_table(&mut eval, vec![Value::NIL])
.expect("first face hash table");
let second = builtin_frame_face_hash_table(&mut eval, vec![Value::NIL])
.expect("second face hash table");
assert_eq!(first, second);
}
#[test]
fn ensure_startup_compat_variables_backfills_missing_xfaces_state() {
crate::test_utils::init_test_tracing();
let mut eval = crate::emacs_core::eval::Context::new();
for name in [
"face-filters-always-match",
"face--new-frame-defaults",
"face-default-stipple",
"scalable-fonts-allowed",
"face-ignored-fonts",
"face-remapping-alist",
"face-font-rescale-alist",
"face-near-same-color-threshold",
"face-font-lax-matched-attributes",
] {
eval.obarray_mut().makunbound(name);
}
ensure_startup_compat_variables(&mut eval);
assert_eq!(
eval.obarray().symbol_value("face-default-stipple").copied(),
Some(Value::string("gray3"))
);
let table = eval
.obarray()
.symbol_value("face--new-frame-defaults")
.copied()
.expect("face hash table backfilled");
if !table.is_hash_table() {
panic!("face--new-frame-defaults must be a hash table");
};
let has_seeded_faces = {
let hash_table = table.as_hash_table().unwrap();
hash_table
.data
.contains_key(&HashKey::Symbol(crate::emacs_core::intern::intern(
"default",
)))
&& hash_table.data.contains_key(&HashKey::Symbol(
crate::emacs_core::intern::intern("mode-line"),
))
};
assert!(
has_seeded_faces,
"face--new-frame-defaults should be preseeded with GNU face entries"
);
}
}