Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gpui-feature-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
persist-credentials: false

- name: Check shell scripts
run: shellcheck scripts/*.sh
run: shellcheck --source-path=scripts -x scripts/*.sh

- name: Check workflow syntax
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
Expand Down
138 changes: 107 additions & 31 deletions crates/gpui-linux/src/linux/text_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::{Context as _, Ok, Result};
use collections::HashMap;
use cosmic_text::{
Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
FontSystem, ShapeBuffer, ShapeLine, SwashCache,
FontSystem, ShapeBuffer, ShapeLine, Stretch, Style, SwashCache, Weight,
};
use gpui::{
Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics, FontRun,
Expand Down Expand Up @@ -59,6 +59,27 @@ struct LoadedFont {
user_fallback_chain: Arc<[(FontId, SharedString)]>,
}

struct FontMatchProperties {
primary_family_name: SharedString,
stretch: Stretch,
style: Style,
weight: Weight,
features: CosmicFontFeatures,
fallback_chain: Arc<[(FontId, SharedString)]>,
}

impl FontMatchProperties {
fn attributes<'a>(&'a self, font_id: FontId, family_name: &'a str) -> Attrs<'a> {
Attrs::new()
.metadata(font_id.0)
.family(Family::Name(family_name))
.stretch(self.stretch)
.style(self.style)
.weight(self.weight)
.font_features(self.features.clone())
}
}

impl CosmicTextSystem {
pub(crate) fn new() -> Self {
// todo(linux) make font loading non-blocking
Expand Down Expand Up @@ -133,6 +154,10 @@ impl PlatformTextSystem for CosmicTextSystem {
Ok(candidates[ix])
}

fn prewarm_fonts(&self, font_ids: &[FontId]) {
self.0.write().prewarm_fonts(font_ids);
}

fn font_metrics(&self, font_id: FontId) -> FontMetrics {
let metrics = self
.0
Expand Down Expand Up @@ -204,6 +229,43 @@ impl CosmicTextSystemState {
&self.loaded_fonts[font_id.0]
}

fn font_match_properties(&self, font_id: FontId) -> Option<FontMatchProperties> {
let loaded_font = self.loaded_font(font_id);
let Some(face) = self.font_system.db().face(loaded_font.font.id()) else {
log::warn!("font face not found in database for font_id {:?}", font_id);
return None;
};
let Some(first_family) = face.families.first() else {
log::warn!("font face has no family names for font_id {:?}", font_id);
return None;
};

Some(FontMatchProperties {
primary_family_name: first_family.0.clone().into(),
stretch: face.stretch,
style: face.style,
weight: face.weight,
features: loaded_font.features.clone(),
fallback_chain: Arc::clone(&loaded_font.user_fallback_chain),
})
}

fn prewarm_fonts(&mut self, font_ids: &[FontId]) {
for &font_id in font_ids {
let Some(properties) = self.font_match_properties(font_id) else {
continue;
};
let primary_attributes =
properties.attributes(font_id, &properties.primary_family_name);
self.font_system.get_font_matches(&primary_attributes);

for (fallback_id, fallback_name) in &*properties.fallback_chain {
let fallback_attributes = properties.attributes(*fallback_id, fallback_name);
self.font_system.get_font_matches(&fallback_attributes);
}
}
}

fn font_weight(&self, font_id: cosmic_text::fontdb::ID) -> cosmic_text::Weight {
self.font_system
.db()
Expand Down Expand Up @@ -547,38 +609,19 @@ impl CosmicTextSystemState {
let mut offs = 0;
for run in font_runs {
let run_end = offs + run.len;
let loaded_font = self.loaded_font(run.font_id);
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();

let primary_family = font.families.first().unwrap().0.clone();
let primary_stretch = font.stretch;
let primary_style = font.style;
let primary_weight = font.weight;
let primary_features = loaded_font.features.clone();
let fallback_chain = Arc::clone(&loaded_font.user_fallback_chain);

let primary_attrs = Attrs::new()
.metadata(run.font_id.0)
.family(Family::Name(&primary_family))
.stretch(primary_stretch)
.style(primary_style)
.weight(primary_weight)
.font_features(primary_features.clone());

let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = fallback_chain
let Some(properties) = self.font_match_properties(run.font_id) else {
offs = run_end;
continue;
};

let primary_attrs = properties.attributes(run.font_id, &properties.primary_family_name);
let fallback_attrs: SmallVec<[Attrs<'_>; 4]> = properties
.fallback_chain
.iter()
.map(|(fallback_id, fallback_family)| {
Attrs::new()
.metadata(fallback_id.0)
.family(Family::Name(fallback_family))
.stretch(primary_stretch)
.style(primary_style)
.weight(primary_weight)
.font_features(primary_features.clone())
})
.map(|(font_id, family_name)| properties.attributes(*font_id, family_name))
.collect();

let spans = if fallback_chain.is_empty() {
let spans = if properties.fallback_chain.is_empty() {
smallvec::smallvec![RunSpan {
start: offs,
end: run_end,
Expand All @@ -587,7 +630,14 @@ impl CosmicTextSystemState {
} else {
let loaded_fonts = &self.loaded_fonts;
let covers = |font_id: FontId, ch: char| charmap_covers(loaded_fonts, font_id, ch);
compute_run_spans(text, offs, run.len, run.font_id, &fallback_chain, &covers)
compute_run_spans(
text,
offs,
run.len,
run.font_id,
&properties.fallback_chain,
&covers,
)
};

for span in spans {
Expand Down Expand Up @@ -823,6 +873,32 @@ mod tests {
include_bytes!("../../test_data/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
const LILEX: &[u8] = include_bytes!("../../test_data/fonts/lilex/Lilex-Regular.ttf");

#[test]
fn prewarm_fonts_is_safe_for_loaded_and_fallback_fonts() {
let text_system = CosmicTextSystem::new();
text_system
.add_fonts(vec![Cow::Borrowed(IBM_PLEX_SANS), Cow::Borrowed(LILEX)])
.unwrap();

let primary_family = family_name(IBM_PLEX_SANS);
let fallback_family = family_name(LILEX);
let mut primary_font = font(primary_family);
primary_font.fallbacks = Some(FontFallbacks::from_fonts(vec![fallback_family]));
let primary_id = text_system.font_id(&primary_font).unwrap();

text_system.prewarm_fonts(&[primary_id]);

let layout = text_system.layout_line(
"AB",
px(16.),
&[FontRun {
len: 2,
font_id: primary_id,
}],
);
assert!(!layout.runs.is_empty());
}

#[test]
fn layout_line_uses_configured_font_fallbacks_for_missing_glyphs() {
let text_system = CosmicTextSystem::new();
Expand Down
58 changes: 57 additions & 1 deletion crates/gpui-linux/src/linux/wayland/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
use ashpd::WindowIdentifier;
use calloop::{
EventLoop, LoopHandle,
ping::Ping,
timer::{TimeoutAction, Timer},
};
use calloop_wayland_source::WaylandSource;
Expand Down Expand Up @@ -181,6 +182,10 @@ fn set_ime_cursor_rectangle_after_done(
}
}

/// Pacing for retry ticks: a fixed 60Hz interval. Retries only occur for throttled or
/// failed-present frames, so matching the output's actual refresh rate wouldn't be observable.
const FRAME_RETRY_INTERVAL: Duration = Duration::from_micros(16_667);

#[derive(Clone)]
pub struct Globals {
pub qh: QueueHandle<WaylandClientStatePtr>,
Expand All @@ -203,6 +208,7 @@ pub struct Globals {
pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>,
pub executor: ForegroundExecutor,
pub frame_ping: Ping,
}

impl Globals {
Expand All @@ -211,6 +217,7 @@ impl Globals {
executor: ForegroundExecutor,
qh: QueueHandle<WaylandClientStatePtr>,
seat: wl_seat::WlSeat,
frame_ping: Ping,
) -> Self {
Globals {
activation: globals.bind(&qh, 1..=1, ()).ok(),
Expand Down Expand Up @@ -244,6 +251,7 @@ impl Globals {
system_bell: globals.bind(&qh, 1..=1, ()).ok(),
executor,
qh,
frame_ping,
}
}
}
Expand Down Expand Up @@ -458,6 +466,45 @@ impl WaylandClientStatePtr {
.expect("The pointer should always be valid when dispatching in wayland")
}

pub fn dispatch_scheduled_frames(&self) {
let Some(client) = self.0.upgrade() else {
return;
};
// Release the client borrow before ticking: the tick re-enters GPUI, which can
// borrow the client again (e.g. IME updates).
let windows = client
.borrow()
.windows
.values()
.cloned()
.collect::<Vec<WaylandWindowStatePtr>>();
for window in windows {
window.scheduled_frame_fired();
}
}

/// Queue a retry tick for `surface_id` one refresh interval from now. An immediate
/// retry would spin against the frame-rate throttle that deferred the draw in the
/// first place.
pub fn schedule_frame_retry(&self, surface_id: &ObjectId) {
let client = self.get_client();
let state = client.borrow();
let surface_id = surface_id.clone();
if let Err(err) = state.loop_handle.insert_source(
Timer::from_duration(FRAME_RETRY_INTERVAL),
move |_, _, this| {
let client = this.get_client();
let window = get_window(&mut client.borrow_mut(), &surface_id);
if let Some(window) = window {
window.retry_timer_fired();
}
TimeoutAction::Drop
},
) {
log::error!("Failed to schedule frame retry: {err}");
}
}

pub fn get_serial(&self, kind: SerialKind) -> Serial {
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
}
Expand Down Expand Up @@ -683,12 +730,21 @@ impl WaylandClient {

let gpu_context = Rc::new(RefCell::new(None));

let (frame_ping, frame_ping_source) =
calloop::ping::make_ping().expect("Failed to create the frame ping");
handle
.insert_source(frame_ping_source, |_, _, client| {
client.dispatch_scheduled_frames();
})
.unwrap();

let seat = seat.unwrap();
let globals = Globals::new(
globals,
common.foreground_executor.clone(),
qh.clone(),
seat.clone(),
frame_ping,
);

let data_device = globals
Expand Down Expand Up @@ -1205,7 +1261,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
drop(state);

if let wl_callback::Event::Done { .. } = event {
window.frame();
window.frame_callback_fired();
}
}
}
Expand Down
Loading
Loading