Present Servo BGRA hardware surfaces
This commit is contained in:
+129
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod client;
|
||||
|
||||
pub(crate) use client::*;
|
||||
@@ -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
@@ -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 }
|
||||
}
|
||||
}
|
||||
+1039
File diff suppressed because it is too large
Load Diff
+581
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2159
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+1219
File diff suppressed because it is too large
Load Diff
+12
@@ -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::*;
|
||||
+2491
File diff suppressed because it is too large
Load Diff
+1270
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
+1670
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user