Present Servo BGRA hardware surfaces

This commit is contained in:
2026-05-13 00:22:00 -04:00
parent bf1ebfb6fe
commit 05a7a0d67f
154 changed files with 84157 additions and 115 deletions
+129
View File
@@ -0,0 +1,129 @@
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use calloop::{
EventLoop,
channel::{self, Sender},
timer::TimeoutAction,
};
use std::{
thread,
time::{Duration, Instant},
};
use util::ResultExt;
struct TimerAfter {
duration: Duration,
runnable: Runnable,
}
pub(crate) struct LinuxDispatcher {
main_sender: Sender<Runnable>,
timer_sender: Sender<TimerAfter>,
background_sender: flume::Sender<Runnable>,
_background_threads: Vec<thread::JoinHandle<()>>,
main_thread_id: thread::ThreadId,
}
impl LinuxDispatcher {
pub fn new(main_sender: Sender<Runnable>) -> Self {
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
let thread_count = std::thread::available_parallelism()
.map(|i| i.get())
.unwrap_or(1);
let mut background_threads = (0..thread_count)
.map(|i| {
let receiver = background_receiver.clone();
std::thread::Builder::new()
.name(format!("Worker-{i}"))
.spawn(move || {
for runnable in receiver {
let start = Instant::now();
runnable.run();
log::trace!(
"background thread {}: ran runnable. took: {:?}",
i,
start.elapsed()
);
}
})
.unwrap()
})
.collect::<Vec<_>>();
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
let timer_thread = std::thread::Builder::new()
.name("Timer".to_owned())
.spawn(|| {
let mut event_loop: EventLoop<()> =
EventLoop::try_new().expect("Failed to initialize timer loop!");
let handle = event_loop.handle();
let timer_handle = event_loop.handle();
handle
.insert_source(timer_channel, move |e, _, _| {
if let channel::Event::Msg(timer) = e {
// This has to be in an option to satisfy the borrow checker. The callback below should only be scheduled once.
let mut runnable = Some(timer.runnable);
timer_handle
.insert_source(
calloop::timer::Timer::from_duration(timer.duration),
move |_, _, _| {
if let Some(runnable) = runnable.take() {
runnable.run();
}
TimeoutAction::Drop
},
)
.expect("Failed to start timer");
}
})
.expect("Failed to start timer thread");
event_loop.run(None, &mut (), |_| {}).log_err();
})
.unwrap();
background_threads.push(timer_thread);
Self {
main_sender,
timer_sender,
background_sender,
_background_threads: background_threads,
main_thread_id: thread::current().id(),
}
}
}
impl PlatformDispatcher for LinuxDispatcher {
fn is_main_thread(&self) -> bool {
thread::current().id() == self.main_thread_id
}
fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
self.background_sender.send(runnable).unwrap();
}
fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.main_sender.send(runnable).unwrap_or_else(|runnable| {
// NOTE: Runnable may wrap a Future that is !Send.
//
// This is usually safe because we only poll it on the main thread.
// However if the send fails, we know that:
// 1. main_receiver has been dropped (which implies the app is shutting down)
// 2. we are on a background thread.
// It is not safe to drop something !Send on the wrong thread, and
// the app will exit soon anyway, so we must forget the runnable.
std::mem::forget(runnable);
});
}
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
self.timer_sender
.send(TimerAfter { duration, runnable })
.ok();
}
}
+3
View File
@@ -0,0 +1,3 @@
mod client;
pub(crate) use client::*;
+134
View File
@@ -0,0 +1,134 @@
use std::cell::RefCell;
use std::rc::Rc;
use calloop::{EventLoop, LoopHandle};
use util::ResultExt;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, LinuxKeyboardLayout, PlatformDisplay,
PlatformKeyboardLayout, WindowParams,
};
pub struct HeadlessClientState {
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
pub(crate) common: LinuxCommon,
}
#[derive(Clone)]
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.ok();
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
event_loop: Some(event_loop),
_loop_handle: handle,
common,
})))
}
}
impl LinuxClient for HeadlessClient {
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(LinuxKeyboardLayout::new("unknown".into()))
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
None
}
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
None
}
#[cfg(feature = "screen-capture")]
fn is_screen_capture_supported(&self) -> bool {
false
}
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
let (mut tx, rx) = futures::channel::oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"Headless mode does not support screen capture."
)))
.ok();
rx
}
fn active_window(&self) -> Option<AnyWindowHandle> {
None
}
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,
_params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode");
}
fn compositor_name(&self) -> &'static str {
"headless"
}
fn set_cursor_style(&self, _style: CursorStyle) {}
fn open_uri(&self, _uri: &str) {}
fn reveal_path(&self, _path: std::path::PathBuf) {}
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
None
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
None
}
fn run(&self) {
let mut event_loop = self
.0
.borrow_mut()
.event_loop
.take()
.expect("App is already running");
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::{PlatformKeyboardLayout, SharedString};
#[derive(Clone)]
pub(crate) struct LinuxKeyboardLayout {
name: SharedString,
}
impl PlatformKeyboardLayout for LinuxKeyboardLayout {
fn id(&self) -> &str {
&self.name
}
fn name(&self) -> &str {
&self.name
}
}
impl LinuxKeyboardLayout {
pub(crate) fn new(name: SharedString) -> Self {
Self { name }
}
}
File diff suppressed because it is too large Load Diff
+581
View File
@@ -0,0 +1,581 @@
use crate::{
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS_X,
SUBPIXEL_VARIANTS_Y, ShapedGlyph, ShapedRun, SharedString, Size, point, size,
};
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,
};
use itertools::Itertools;
use parking_lot::RwLock;
use pathfinder_geometry::{
rect::{RectF, RectI},
vector::{Vector2F, Vector2I},
};
use smallvec::SmallVec;
use std::{borrow::Cow, sync::Arc};
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct FontKey {
family: SharedString,
features: FontFeatures,
}
impl FontKey {
fn new(family: SharedString, features: FontFeatures) -> Self {
Self { family, features }
}
}
struct CosmicTextSystemState {
swash_cache: SwashCache,
font_system: FontSystem,
scratch: ShapeBuffer,
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
loaded_fonts: Vec<LoadedFont>,
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
/// for every font face in a family.
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
}
struct LoadedFont {
font: Arc<CosmicTextFont>,
features: CosmicFontFeatures,
is_known_emoji_font: bool,
}
impl CosmicTextSystem {
pub(crate) fn new() -> Self {
// todo(linux) make font loading non-blocking
let mut font_system = FontSystem::new();
Self(RwLock::new(CosmicTextSystemState {
font_system,
swash_cache: SwashCache::new(),
scratch: ShapeBuffer::default(),
loaded_fonts: Vec::new(),
font_ids_by_family_cache: HashMap::default(),
}))
}
}
impl Default for CosmicTextSystem {
fn default() -> Self {
Self::new()
}
}
impl PlatformTextSystem for CosmicTextSystem {
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.0.write().add_fonts(fonts)
}
fn all_font_names(&self) -> Vec<String> {
let mut result = self
.0
.read()
.font_system
.db()
.faces()
.filter_map(|face| face.families.first().map(|family| family.0.clone()))
.collect_vec();
result.sort();
result.dedup();
result
}
fn font_id(&self, font: &Font) -> Result<FontId> {
// todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
let mut state = self.0.write();
let key = FontKey::new(font.family.clone(), font.features.clone());
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
font_ids.as_slice()
} else {
let font_ids = state.load_family(&font.family, &font.features)?;
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
state.font_ids_by_family_cache[&key].as_ref()
};
// todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
let candidate_properties = candidates
.iter()
.map(|font_id| {
let database_id = state.loaded_font(*font_id).font.id();
let face_info = state.font_system.db().face(database_id).expect("");
face_info_into_properties(face_info)
})
.collect::<SmallVec<[_; 4]>>();
let ix =
font_kit::matching::find_best_match(&candidate_properties, &font_into_properties(font))
.context("requested font family contains no font matching the other parameters")?;
Ok(candidates[ix])
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
let metrics = self
.0
.read()
.loaded_font(font_id)
.font
.as_swash()
.metrics(&[]);
FontMetrics {
units_per_em: metrics.units_per_em as u32,
ascent: metrics.ascent,
descent: -metrics.descent, // todo(linux) confirm this is correct
line_gap: metrics.leading,
underline_position: metrics.underline_offset,
underline_thickness: metrics.stroke_size,
cap_height: metrics.cap_height,
x_height: metrics.x_height,
// todo(linux): Compute this correctly
bounding_box: Bounds {
origin: point(0.0, 0.0),
size: size(metrics.max_width, metrics.ascent + metrics.descent),
},
}
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
let lock = self.0.read();
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
let glyph_id = glyph_id.0 as u16;
// todo(linux): Compute this correctly
// see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
Ok(Bounds {
origin: point(0.0, 0.0),
size: size(
glyph_metrics.advance_width(glyph_id),
glyph_metrics.advance_height(glyph_id),
),
})
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
self.0.read().advance(font_id, glyph_id)
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
self.0.read().glyph_for_char(font_id, ch)
}
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.0.write().raster_bounds(params)
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.0.write().rasterize_glyph(params, raster_bounds)
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, runs)
}
}
impl CosmicTextSystemState {
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
&self.loaded_fonts[font_id.0]
}
#[profiling::function]
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
let db = self.font_system.db_mut();
for bytes in fonts {
match bytes {
Cow::Borrowed(embedded_font) => {
db.load_font_data(embedded_font.to_vec());
}
Cow::Owned(bytes) => {
db.load_font_data(bytes);
}
}
}
Ok(())
}
#[profiling::function]
fn load_family(
&mut self,
name: &str,
features: &FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
// TODO: Determine the proper system UI font.
let name = crate::text_system::font_name_with_fallbacks(name, "IBM Plex Sans");
let families = self
.font_system
.db()
.faces()
.filter(|face| face.families.iter().any(|family| *name == family.0))
.map(|face| (face.id, face.post_script_name.clone()))
.collect::<SmallVec<[_; 4]>>();
let mut loaded_font_ids = SmallVec::new();
for (font_id, postscript_name) in families {
let font = self
.font_system
.get_font(font_id)
.context("Could not load font")?;
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
let allowed_bad_font_names = [
"SegoeFluentIcons", // NOTE: Segoe fluent icons postscript name is inconsistent
"Segoe Fluent Icons",
];
if font.as_swash().charmap().map('m') == 0
&& !allowed_bad_font_names.contains(&postscript_name.as_str())
{
self.font_system.db_mut().remove_face(font.id());
continue;
};
let font_id = FontId(self.loaded_fonts.len());
loaded_font_ids.push(font_id);
self.loaded_fonts.push(LoadedFont {
font,
features: features.try_into()?,
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
});
}
Ok(loaded_font_ids)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
Ok(Size {
width: glyph_metrics.advance_width(glyph_id.0 as u16),
height: glyph_metrics.advance_height(glyph_id.0 as u16),
})
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
if glyph_id == 0 {
None
} else {
Some(GlyphId(glyph_id.into()))
}
}
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let font = &self.loaded_fonts[params.font_id.0].font;
let subpixel_shift = point(
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
);
let image = self
.swash_cache
.get_image(
&mut self.font_system,
CacheKey::new(
font.id(),
params.glyph_id.0 as u16,
(params.font_size * params.scale_factor).into(),
(subpixel_shift.x, subpixel_shift.y.trunc()),
cosmic_text::CacheKeyFlags::empty(),
)
.0,
)
.clone()
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
Ok(Bounds {
origin: point(image.placement.left.into(), (-image.placement.top).into()),
size: size(image.placement.width.into(), image.placement.height.into()),
})
}
#[profiling::function]
fn rasterize_glyph(
&mut self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
anyhow::bail!("glyph bounds are empty");
} else {
let bitmap_size = glyph_bounds.size;
let font = &self.loaded_fonts[params.font_id.0].font;
let subpixel_shift = point(
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
);
let mut image = self
.swash_cache
.get_image(
&mut self.font_system,
CacheKey::new(
font.id(),
params.glyph_id.0 as u16,
(params.font_size * params.scale_factor).into(),
(subpixel_shift.x, subpixel_shift.y.trunc()),
cosmic_text::CacheKeyFlags::empty(),
)
.0,
)
.clone()
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
if params.is_emoji {
// Convert from RGBA to BGRA.
for pixel in image.data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
}
Ok((bitmap_size, image.data))
}
}
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
/// yet have an entry for this fallback font, and so one is added.
///
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
if let Some(ix) = self
.loaded_fonts
.iter()
.position(|loaded_font| loaded_font.font.id() == id)
{
FontId(ix)
} else {
let font = self.font_system.get_font(id).unwrap();
let face = self.font_system.db().face(id).unwrap();
let font_id = FontId(self.loaded_fonts.len());
self.loaded_fonts.push(LoadedFont {
font,
features: CosmicFontFeatures::new(),
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
});
font_id
}
}
#[profiling::function]
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
let mut attrs_list = AttrsList::new(&Attrs::new());
let mut offs = 0;
for run in font_runs {
let loaded_font = self.loaded_font(run.font_id);
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
attrs_list.add_span(
offs..(offs + run.len),
&Attrs::new()
.metadata(run.font_id.0)
.family(Family::Name(&font.families.first().unwrap().0))
.stretch(font.stretch)
.style(font.style)
.weight(font.weight)
.font_features(loaded_font.features.clone()),
);
offs += run.len;
}
let line = ShapeLine::new(
&mut self.font_system,
text,
&attrs_list,
cosmic_text::Shaping::Advanced,
4,
);
let mut layout_lines = Vec::with_capacity(1);
line.layout_to_buffer(
&mut self.scratch,
font_size.0,
None, // We do our own wrapping
cosmic_text::Wrap::None,
None,
&mut layout_lines,
None,
);
let layout = layout_lines.first().unwrap();
let mut runs: Vec<ShapedRun> = Vec::new();
for glyph in &layout.glyphs {
let mut font_id = FontId(glyph.metadata);
let mut loaded_font = self.loaded_font(font_id);
if loaded_font.font.id() != glyph.font_id {
font_id = self.font_id_for_cosmic_id(glyph.font_id);
loaded_font = self.loaded_font(font_id);
}
let is_emoji = loaded_font.is_known_emoji_font;
// HACK: Prevent crash caused by variation selectors.
if glyph.glyph_id == 3 && is_emoji {
continue;
}
let shaped_glyph = ShapedGlyph {
id: GlyphId(glyph.glyph_id as u32),
position: point(glyph.x.into(), glyph.y.into()),
index: glyph.start,
is_emoji,
};
if let Some(last_run) = runs
.last_mut()
.filter(|last_run| last_run.font_id == font_id)
{
last_run.glyphs.push(shaped_glyph);
} else {
runs.push(ShapedRun {
font_id,
glyphs: vec![shaped_glyph],
});
}
}
LineLayout {
font_size,
width: layout.w.into(),
ascent: layout.max_ascent.into(),
descent: layout.max_descent.into(),
runs,
len: text.len(),
}
}
}
impl TryFrom<&FontFeatures> for CosmicFontFeatures {
type Error = anyhow::Error;
fn try_from(features: &FontFeatures) -> Result<Self> {
let mut result = CosmicFontFeatures::new();
for feature in features.0.iter() {
let name_bytes: [u8; 4] = feature
.0
.as_bytes()
.try_into()
.context("Incorrect feature flag format")?;
let tag = cosmic_text::FeatureTag::new(&name_bytes);
result.set(tag, feature.1);
}
Ok(result)
}
}
impl From<RectF> for Bounds<f32> {
fn from(rect: RectF) -> Self {
Bounds {
origin: point(rect.origin_x(), rect.origin_y()),
size: size(rect.width(), rect.height()),
}
}
}
impl From<RectI> for Bounds<DevicePixels> {
fn from(rect: RectI) -> Self {
Bounds {
origin: point(DevicePixels(rect.origin_x()), DevicePixels(rect.origin_y())),
size: size(DevicePixels(rect.width()), DevicePixels(rect.height())),
}
}
}
impl From<Vector2I> for Size<DevicePixels> {
fn from(value: Vector2I) -> Self {
size(value.x().into(), value.y().into())
}
}
impl From<RectI> for Bounds<i32> {
fn from(rect: RectI) -> Self {
Bounds {
origin: point(rect.origin_x(), rect.origin_y()),
size: size(rect.width(), rect.height()),
}
}
}
impl From<Point<u32>> for Vector2I {
fn from(size: Point<u32>) -> Self {
Vector2I::new(size.x as i32, size.y as i32)
}
}
impl From<Vector2F> for Size<f32> {
fn from(vec: Vector2F) -> Self {
size(vec.x(), vec.y())
}
}
impl From<FontWeight> for cosmic_text::Weight {
fn from(value: FontWeight) -> Self {
cosmic_text::Weight(value.0 as u16)
}
}
impl From<FontStyle> for cosmic_text::Style {
fn from(style: FontStyle) -> Self {
match style {
FontStyle::Normal => cosmic_text::Style::Normal,
FontStyle::Italic => cosmic_text::Style::Italic,
FontStyle::Oblique => cosmic_text::Style::Oblique,
}
}
}
fn font_into_properties(font: &crate::Font) -> font_kit::properties::Properties {
font_kit::properties::Properties {
style: match font.style {
crate::FontStyle::Normal => font_kit::properties::Style::Normal,
crate::FontStyle::Italic => font_kit::properties::Style::Italic,
crate::FontStyle::Oblique => font_kit::properties::Style::Oblique,
},
weight: font_kit::properties::Weight(font.weight.0),
stretch: Default::default(),
}
}
fn face_info_into_properties(
face_info: &cosmic_text::fontdb::FaceInfo,
) -> font_kit::properties::Properties {
font_kit::properties::Properties {
style: match face_info.style {
cosmic_text::Style::Normal => font_kit::properties::Style::Normal,
cosmic_text::Style::Italic => font_kit::properties::Style::Italic,
cosmic_text::Style::Oblique => font_kit::properties::Style::Oblique,
},
// both libs use the same values for weight
weight: font_kit::properties::Weight(face_info.weight.0.into()),
stretch: match face_info.stretch {
cosmic_text::Stretch::Condensed => font_kit::properties::Stretch::CONDENSED,
cosmic_text::Stretch::Expanded => font_kit::properties::Stretch::EXPANDED,
cosmic_text::Stretch::ExtraCondensed => font_kit::properties::Stretch::EXTRA_CONDENSED,
cosmic_text::Stretch::ExtraExpanded => font_kit::properties::Stretch::EXTRA_EXPANDED,
cosmic_text::Stretch::Normal => font_kit::properties::Stretch::NORMAL,
cosmic_text::Stretch::SemiCondensed => font_kit::properties::Stretch::SEMI_CONDENSED,
cosmic_text::Stretch::SemiExpanded => font_kit::properties::Stretch::SEMI_EXPANDED,
cosmic_text::Stretch::UltraCondensed => font_kit::properties::Stretch::ULTRA_CONDENSED,
cosmic_text::Stretch::UltraExpanded => font_kit::properties::Stretch::ULTRA_EXPANDED,
},
}
}
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
// TODO: Include other common emoji fonts
postscript_name == "NotoColorEmoji"
}
+46
View File
@@ -0,0 +1,46 @@
mod client;
mod clipboard;
mod cursor;
mod display;
mod serial;
mod window;
pub(crate) use client::*;
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
use crate::CursorStyle;
impl CursorStyle {
pub(super) fn to_shape(self) -> Shape {
match self {
CursorStyle::Arrow => Shape::Default,
CursorStyle::IBeam => Shape::Text,
CursorStyle::Crosshair => Shape::Crosshair,
CursorStyle::ClosedHand => Shape::Grabbing,
CursorStyle::OpenHand => Shape::Grab,
CursorStyle::PointingHand => Shape::Pointer,
CursorStyle::ResizeLeft => Shape::WResize,
CursorStyle::ResizeRight => Shape::EResize,
CursorStyle::ResizeLeftRight => Shape::EwResize,
CursorStyle::ResizeUp => Shape::NResize,
CursorStyle::ResizeDown => Shape::SResize,
CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
CursorStyle::ResizeColumn => Shape::ColResize,
CursorStyle::ResizeRow => Shape::RowResize,
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
CursorStyle::DragLink => Shape::Alias,
CursorStyle::DragCopy => Shape::Copy,
CursorStyle::ContextualMenu => Shape::ContextMenu,
CursorStyle::None => {
#[cfg(debug_assertions)]
panic!("CursorStyle::None should be handled separately in the client");
#[cfg(not(debug_assertions))]
Shape::Default
}
}
}
}
File diff suppressed because it is too large Load Diff
+262
View File
@@ -0,0 +1,262 @@
use std::{
fs::File,
io::{ErrorKind, Write},
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
};
use calloop::{LoopHandle, PostAction};
use filedescriptor::Pipe;
use strum::IntoEnumIterator;
use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
use crate::{
ClipboardEntry, ClipboardItem, Image, ImageFormat, WaylandClientStatePtr, hash,
platform::linux::platform::read_fd,
};
/// Text mime types that we'll offer to other programs.
pub(crate) const TEXT_MIME_TYPES: [&str; 3] =
["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
/// Text mime types that we'll accept from other programs.
pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
pub(crate) struct Clipboard {
connection: Connection,
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
self_mime: String,
// Internal clipboard
contents: Option<ClipboardItem>,
primary_contents: Option<ClipboardItem>,
// External clipboard
cached_read: Option<ClipboardItem>,
current_offer: Option<DataOffer<WlDataOffer>>,
cached_primary_read: Option<ClipboardItem>,
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
}
pub(crate) trait ReceiveData {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>);
}
impl ReceiveData for WlDataOffer {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
self.receive(mime_type, fd);
}
}
impl ReceiveData for ZwpPrimarySelectionOfferV1 {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
self.receive(mime_type, fd);
}
}
#[derive(Clone, Debug)]
/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
pub(crate) struct DataOffer<T: ReceiveData> {
pub inner: T,
mime_types: Vec<String>,
}
impl<T: ReceiveData> DataOffer<T> {
pub fn new(offer: T) -> Self {
Self {
inner: offer,
mime_types: Vec::new(),
}
}
pub fn add_mime_type(&mut self, mime_type: String) {
self.mime_types.push(mime_type)
}
fn has_mime_type(&self, mime_type: &str) -> bool {
self.mime_types.iter().any(|t| t == mime_type)
}
fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option<Vec<u8>> {
let pipe = Pipe::new().unwrap();
self.inner.receive_data(mime_type.to_string(), unsafe {
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
});
let fd = pipe.read;
drop(pipe.write);
connection.flush().unwrap();
match unsafe { read_fd(fd) } {
Ok(bytes) => Some(bytes),
Err(err) => {
log::error!("error reading clipboard pipe: {err:?}");
None
}
}
}
fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
let mime_type = self.mime_types.iter().find(|&mime_type| {
ALLOWED_TEXT_MIME_TYPES
.iter()
.any(|&allowed| allowed == mime_type)
})?;
let bytes = self.read_bytes(connection, mime_type)?;
let text_content = match String::from_utf8(bytes) {
Ok(content) => content,
Err(e) => {
log::error!("Failed to convert clipboard content to UTF-8: {}", e);
return None;
}
};
// Normalize the text to unix line endings, otherwise
// copying from eg: firefox inserts a lot of blank
// lines, and that is super annoying.
let result = text_content.replace("\r\n", "\n");
Some(ClipboardItem::new_string(result))
}
fn read_image(&self, connection: &Connection) -> Option<ClipboardItem> {
for format in ImageFormat::iter() {
let mime_type = format.mime_type();
if !self.has_mime_type(mime_type) {
continue;
}
if let Some(bytes) = self.read_bytes(connection, mime_type) {
let id = hash(&bytes);
return Some(ClipboardItem {
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
});
}
}
None
}
}
impl Clipboard {
pub fn new(
connection: Connection,
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
) -> Self {
Self {
connection,
loop_handle,
self_mime: format!("pid/{}", std::process::id()),
contents: None,
primary_contents: None,
cached_read: None,
current_offer: None,
cached_primary_read: None,
current_primary_offer: None,
}
}
pub fn set(&mut self, item: ClipboardItem) {
self.contents = Some(item);
}
pub fn set_primary(&mut self, item: ClipboardItem) {
self.primary_contents = Some(item);
}
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
self.cached_read = None;
self.current_offer = data_offer;
}
pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
self.cached_primary_read = None;
self.current_primary_offer = data_offer;
}
pub fn self_mime(&self) -> String {
self.self_mime.clone()
}
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) {
self.send_internal(fd, text.as_bytes().to_owned());
}
}
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
if let Some(text) = self
.primary_contents
.as_ref()
.and_then(|contents| contents.text())
{
self.send_internal(fd, text.as_bytes().to_owned());
}
}
pub fn read(&mut self) -> Option<ClipboardItem> {
let offer = self.current_offer.as_ref()?;
if let Some(cached) = self.cached_read.clone() {
return Some(cached);
}
if offer.has_mime_type(&self.self_mime) {
return self.contents.clone();
}
let item = offer
.read_text(&self.connection)
.or_else(|| offer.read_image(&self.connection))?;
self.cached_read = Some(item.clone());
Some(item)
}
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
let offer = self.current_primary_offer.as_ref()?;
if let Some(cached) = self.cached_primary_read.clone() {
return Some(cached);
}
if offer.has_mime_type(&self.self_mime) {
return self.primary_contents.clone();
}
let item = offer
.read_text(&self.connection)
.or_else(|| offer.read_image(&self.connection))?;
self.cached_primary_read = Some(item.clone());
Some(item)
}
fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
let mut written = 0;
self.loop_handle
.insert_source(
calloop::generic::Generic::new(
File::from(fd),
calloop::Interest::WRITE,
calloop::Mode::Level,
),
move |_, file, _| {
let mut file = unsafe { file.get_mut() };
loop {
match file.write(&bytes[written..]) {
Ok(n) if written + n == bytes.len() => {
written += n;
break Ok(PostAction::Remove);
}
Ok(n) => written += n,
Err(err) if err.kind() == ErrorKind::WouldBlock => {
break Ok(PostAction::Continue);
}
Err(_) => break Ok(PostAction::Remove),
}
}
},
)
.unwrap();
}
}
+152
View File
@@ -0,0 +1,152 @@
use crate::Globals;
use crate::platform::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning};
use anyhow::{Context as _, anyhow};
use util::ResultExt;
use wayland_client::Connection;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm};
use wayland_cursor::{CursorImageBuffer, CursorTheme};
pub(crate) struct Cursor {
loaded_theme: Option<LoadedTheme>,
size: u32,
scaled_size: u32,
surface: WlSurface,
shm: WlShm,
connection: Connection,
}
pub(crate) struct LoadedTheme {
theme: CursorTheme,
name: Option<String>,
scaled_size: u32,
}
impl Drop for Cursor {
fn drop(&mut self) {
self.loaded_theme.take();
self.surface.destroy();
}
}
impl Cursor {
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
let mut this = Self {
loaded_theme: None,
size,
scaled_size: size,
surface: globals.compositor.create_surface(&globals.qh, ()),
shm: globals.shm.clone(),
connection: connection.clone(),
};
this.set_theme_internal(None);
this
}
fn set_theme_internal(&mut self, theme_name: Option<String>) {
if let Some(loaded_theme) = self.loaded_theme.as_ref()
&& loaded_theme.name == theme_name
&& loaded_theme.scaled_size == self.scaled_size
{
return;
}
let result = if let Some(theme_name) = theme_name.as_ref() {
CursorTheme::load_from_name(
&self.connection,
self.shm.clone(),
theme_name,
self.scaled_size,
)
} else {
CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size)
};
if let Some(theme) = result
.context("Wayland: Failed to load cursor theme")
.log_err()
{
self.loaded_theme = Some(LoadedTheme {
theme,
name: theme_name,
scaled_size: self.scaled_size,
});
}
}
pub fn set_theme(&mut self, theme_name: String) {
self.set_theme_internal(Some(theme_name));
}
fn set_scaled_size(&mut self, scaled_size: u32) {
self.scaled_size = scaled_size;
let theme_name = self
.loaded_theme
.as_ref()
.and_then(|loaded_theme| loaded_theme.name.clone());
self.set_theme_internal(theme_name);
}
pub fn set_size(&mut self, size: u32) {
self.size = size;
self.set_scaled_size(size);
}
pub fn set_icon(
&mut self,
wl_pointer: &WlPointer,
serial_id: u32,
mut cursor_icon_names: &[&str],
scale: i32,
) {
self.set_scaled_size(self.size * scale as u32);
let Some(loaded_theme) = &mut self.loaded_theme else {
log::warn!("Wayland: Unable to load cursor themes");
return;
};
let mut theme = &mut loaded_theme.theme;
let mut buffer: &CursorImageBuffer;
'outer: {
for cursor_icon_name in cursor_icon_names {
if let Some(cursor) = theme.get_cursor(cursor_icon_name) {
buffer = &cursor[0];
break 'outer;
}
}
if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) {
buffer = &cursor[0];
log_cursor_icon_warning(anyhow!(
"wayland: Unable to get cursor icon {:?}. \
Using default cursor icon: '{}'",
cursor_icon_names,
DEFAULT_CURSOR_ICON_NAME
));
} else {
log_cursor_icon_warning(anyhow!(
"wayland: Unable to fallback on default cursor icon '{}' for theme '{}'",
DEFAULT_CURSOR_ICON_NAME,
loaded_theme.name.as_deref().unwrap_or("default")
));
return;
}
}
let (width, height) = buffer.dimensions();
let (hot_x, hot_y) = buffer.hotspot();
self.surface.set_buffer_scale(scale);
wl_pointer.set_cursor(
serial_id,
Some(&self.surface),
hot_x as i32 / scale,
hot_y as i32 / scale,
);
self.surface.attach(Some(buffer), 0, 0);
self.surface.damage(0, 0, width as i32, height as i32);
self.surface.commit();
}
}
+42
View File
@@ -0,0 +1,42 @@
use std::{
fmt::Debug,
hash::{Hash, Hasher},
};
use anyhow::Context as _;
use uuid::Uuid;
use wayland_backend::client::ObjectId;
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay};
#[derive(Debug, Clone)]
pub(crate) struct WaylandDisplay {
/// The ID of the wl_output object
pub id: ObjectId,
pub name: Option<String>,
pub bounds: Bounds<Pixels>,
}
impl Hash for WaylandDisplay {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PlatformDisplay for WaylandDisplay {
fn id(&self) -> DisplayId {
DisplayId(self.id.protocol_id())
}
fn uuid(&self) -> anyhow::Result<Uuid> {
let name = self
.name
.as_ref()
.context("Wayland display does not have a name")?;
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
}
+49
View File
@@ -0,0 +1,49 @@
use collections::HashMap;
#[derive(Debug, Hash, PartialEq, Eq)]
pub(crate) enum SerialKind {
DataDevice,
InputMethod,
MouseEnter,
MousePress,
KeyPress,
}
#[derive(Debug)]
struct SerialData {
serial: u32,
}
impl SerialData {
fn new(value: u32) -> Self {
Self { serial: value }
}
}
#[derive(Debug)]
/// Helper for tracking of different serial kinds.
pub(crate) struct SerialTracker {
serials: HashMap<SerialKind, SerialData>,
}
impl SerialTracker {
pub fn new() -> Self {
Self {
serials: HashMap::default(),
}
}
pub fn update(&mut self, kind: SerialKind, value: u32) {
self.serials.insert(kind, SerialData::new(value));
}
/// Returns the latest tracked serial of the provided [`SerialKind`]
///
/// Will return 0 if not tracked.
pub fn get(&self, kind: SerialKind) -> u32 {
self.serials
.get(&kind)
.map(|serial_data| serial_data.serial)
.unwrap_or(0)
}
}
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
mod client;
mod clipboard;
mod display;
mod event;
mod window;
mod xim_handler;
pub(crate) use client::*;
pub(crate) use display::*;
pub(crate) use event::*;
pub(crate) use window::*;
pub(crate) use xim_handler::*;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
use anyhow::Context as _;
use uuid::Uuid;
use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection};
use crate::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px};
#[derive(Debug)]
pub(crate) struct X11Display {
x_screen_index: usize,
bounds: Bounds<Pixels>,
uuid: Uuid,
}
impl X11Display {
pub(crate) fn new(
xcb: &XCBConnection,
scale_factor: f32,
x_screen_index: usize,
) -> anyhow::Result<Self> {
let screen = xcb
.setup()
.roots
.get(x_screen_index)
.with_context(|| format!("No screen found with index {x_screen_index}"))?;
Ok(Self {
x_screen_index,
bounds: Bounds {
origin: Default::default(),
size: Size {
width: px(screen.width_in_pixels as f32 / scale_factor),
height: px(screen.height_in_pixels as f32 / scale_factor),
},
},
uuid: Uuid::from_bytes([0; 16]),
})
}
}
impl PlatformDisplay for X11Display {
fn id(&self) -> DisplayId {
DisplayId(self.x_screen_index as u32)
}
fn uuid(&self) -> anyhow::Result<Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
}
+154
View File
@@ -0,0 +1,154 @@
use x11rb::protocol::{
xinput,
xproto::{self, ModMask},
};
use crate::{Modifiers, MouseButton, NavigationDirection};
pub(crate) enum ButtonOrScroll {
Button(MouseButton),
Scroll(ScrollDirection),
}
pub(crate) enum ScrollDirection {
Up,
Down,
Left,
Right,
}
pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option<ButtonOrScroll> {
Some(match detail {
1 => ButtonOrScroll::Button(MouseButton::Left),
2 => ButtonOrScroll::Button(MouseButton::Middle),
3 => ButtonOrScroll::Button(MouseButton::Right),
4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
5 => ButtonOrScroll::Scroll(ScrollDirection::Down),
6 => ButtonOrScroll::Scroll(ScrollDirection::Left),
7 => ButtonOrScroll::Scroll(ScrollDirection::Right),
8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)),
9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)),
_ => return None,
})
}
pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
Modifiers {
control: state.contains(xproto::KeyButMask::CONTROL),
alt: state.contains(xproto::KeyButMask::MOD1),
shift: state.contains(xproto::KeyButMask::SHIFT),
platform: state.contains(xproto::KeyButMask::MOD4),
function: false,
}
}
pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers {
Modifiers {
control: modifier_info.effective as u16 & ModMask::CONTROL.bits()
== ModMask::CONTROL.bits(),
alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(),
shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(),
platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(),
function: false,
}
}
pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton> {
Some(if button_mask & 2 == 2 {
MouseButton::Left
} else if button_mask & 4 == 4 {
MouseButton::Middle
} else if button_mask & 8 == 8 {
MouseButton::Right
} else {
return None;
})
}
pub(crate) fn get_valuator_axis_index(
valuator_mask: &Vec<u32>,
valuator_number: u16,
) -> Option<usize> {
// XInput valuator masks have a 1 at the bit indexes corresponding to each
// valuator present in this event's axisvalues. Axisvalues is ordered from
// lowest valuator number to highest, so counting bits before the 1 bit for
// this valuator yields the index in axisvalues.
if bit_is_set_in_vec(valuator_mask, valuator_number) {
Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize)
} else {
None
}
}
/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`.
fn popcount_upto_bit_index(bit_vec: &Vec<u32>, bit_index: u16) -> u32 {
let array_index = bit_index as usize / 32;
let popcount: u32 = bit_vec
.get(array_index)
.map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones());
if array_index == 0 {
popcount
} else {
// Valuator numbers over 32 probably never occur for scroll position, but may as well
// support it.
let leading_popcount: u32 = bit_vec
.iter()
.take(array_index)
.map(|bits| bits.count_ones())
.sum();
popcount + leading_popcount
}
}
fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
let array_index = bit_index as usize / 32;
bit_vec
.get(array_index)
.is_some_and(|bits| bit_is_set(*bits, bit_index % 32))
}
fn bit_is_set(bits: u32, bit_index: u16) -> bool {
bits & (1 << bit_index) != 0
}
/// Sets every bit with `i >= bit_index` to 0.
fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 {
if bit_index == 0 {
0
} else if bit_index >= 32 {
u32::MAX
} else {
bits & ((1 << bit_index) - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_valuator_axis_index() {
assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0));
assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1));
assert!(get_valuator_axis_index(&vec![0b11], 2) == None);
assert!(get_valuator_axis_index(&vec![0b100], 0) == None);
assert!(get_valuator_axis_index(&vec![0b100], 1) == None);
assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0));
assert!(get_valuator_axis_index(&vec![0b100], 3) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0));
assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3));
}
}
File diff suppressed because it is too large Load Diff
+133
View File
@@ -0,0 +1,133 @@
use std::default::Default;
use x11rb::protocol::{Event, xproto};
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle};
pub enum XimCallbackEvent {
XimXEvent(x11rb::protocol::Event),
XimPreeditEvent(xproto::Window, String),
XimCommitEvent(xproto::Window, String),
}
pub struct XimHandler {
pub im_id: u16,
pub ic_id: u16,
pub connected: bool,
pub window: xproto::Window,
pub last_callback_event: Option<XimCallbackEvent>,
}
impl XimHandler {
pub fn new() -> Self {
Self {
im_id: Default::default(),
ic_id: Default::default(),
connected: false,
window: Default::default(),
last_callback_event: None,
}
}
}
impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler {
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
client.open("C")
}
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
self.im_id = input_method_id;
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
}
fn handle_get_im_values(
&mut self,
client: &mut C,
input_method_id: u16,
_attributes: AHashMap<AttributeName, Vec<u8>>,
) -> Result<(), ClientError> {
let ic_attributes = client
.build_ic_attributes()
.push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS)
.push(AttributeName::ClientWindow, self.window)
.push(AttributeName::FocusWindow, self.window)
.build();
client.create_ic(input_method_id, ic_attributes)
}
fn handle_create_ic(
&mut self,
_client: &mut C,
_input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
self.connected = true;
self.ic_id = input_context_id;
Ok(())
}
fn handle_commit(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
Ok(())
}
fn handle_forward_event(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_flag: xim::ForwardEventFlag,
xev: C::XEvent,
) -> Result<(), ClientError> {
match xev.response_type {
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
}
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
self.last_callback_event =
Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
}
_ => {}
}
Ok(())
}
fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> {
client.disconnect()
}
fn handle_preedit_draw(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_caret: i32,
_chg_first: i32,
_chg_len: i32,
_status: xim::PreeditDrawStatus,
preedit_string: &str,
_feedbacks: Vec<xim::Feedback>,
) -> Result<(), ClientError> {
// XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text
// XIMUnderline: 2, XIMSecondary: 16: underlined text
// XIMHighlight: 4: normal text
// XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
// but interchangeable as above
// Currently there's no way to support these.
self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
Ok(())
}
}
@@ -0,0 +1,171 @@
//! Provides a [calloop] event source from [XDG Desktop Portal] events
//!
//! This module uses the [ashpd] crate
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::Channel;
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use smol::stream::StreamExt;
use crate::{BackgroundExecutor, WindowAppearance};
pub enum Event {
WindowAppearance(WindowAppearance),
#[cfg_attr(feature = "x11", allow(dead_code))]
CursorTheme(String),
#[cfg_attr(feature = "x11", allow(dead_code))]
CursorSize(u32),
}
pub struct XDPEventSource {
channel: Channel<Event>,
}
impl XDPEventSource {
pub fn new(executor: &BackgroundExecutor) -> Self {
let (sender, channel) = calloop::channel::channel();
let background = executor.clone();
executor
.spawn(async move {
let settings = Settings::new().await?;
if let Ok(initial_appearance) = settings.color_scheme().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
initial_appearance,
)))?;
}
if let Ok(initial_theme) = settings
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
.await
{
sender.send(Event::CursorTheme(initial_theme))?;
}
// If u32 is used here, it throws invalid type error
if let Ok(initial_size) = settings
.read::<i32>("org.gnome.desktop.interface", "cursor-size")
.await
{
sender.send(Event::CursorSize(initial_size as u32))?;
}
if let Ok(mut cursor_theme_changed) = settings
.receive_setting_changed_with_args(
"org.gnome.desktop.interface",
"cursor-theme",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(theme) = cursor_theme_changed.next().await {
let theme = theme?;
sender.send(Event::CursorTheme(theme))?;
}
anyhow::Ok(())
})
.detach();
}
if let Ok(mut cursor_size_changed) = settings
.receive_setting_changed_with_args::<i32>(
"org.gnome.desktop.interface",
"cursor-size",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(size) = cursor_size_changed.next().await {
let size = size?;
sender.send(Event::CursorSize(size as u32))?;
}
anyhow::Ok(())
})
.detach();
}
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = appearance_changed.next().await {
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
scheme,
)))?;
}
anyhow::Ok(())
})
.detach();
Self { channel }
}
}
impl EventSource for XDPEventSource {
type Event = Event;
type Metadata = ();
type Ret = ();
type Error = anyhow::Error;
fn process_events<F>(
&mut self,
readiness: Readiness,
token: Token,
mut callback: F,
) -> Result<PostAction, Self::Error>
where
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
{
self.channel.process_events(readiness, token, |evt, _| {
if let calloop::channel::Event::Msg(msg) = evt {
(callback)(msg, &mut ())
}
})?;
Ok(PostAction::Continue)
}
fn register(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.register(poll, token_factory)?;
Ok(())
}
fn reregister(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.reregister(poll, token_factory)?;
Ok(())
}
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
self.channel.unregister(poll)?;
Ok(())
}
}
impl WindowAppearance {
fn from_native(cs: ColorScheme) -> WindowAppearance {
match cs {
ColorScheme::PreferDark => WindowAppearance::Dark,
ColorScheme::PreferLight => WindowAppearance::Light,
ColorScheme::NoPreference => WindowAppearance::Light,
}
}
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
fn set_native(&mut self, cs: ColorScheme) {
*self = Self::from_native(cs);
}
}