#![cfg(all(unix_desktop, feature = "gtk-native", widgets_unstripped))]
use super::types::{LinuxNativeState, LinuxPlatform};
use crate::core::{Color, ObjectId, Point, Rect, Size};
use crate::core::MutexExt;
use crate::event::Event;
use gtk::cairo;
use gtk::gdk;
use gtk::glib;
use gtk::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
pub(crate) fn mount_canvas(
platform: &LinuxPlatform,
parent: ObjectId,
id: ObjectId,
rect: Rect,
) -> bool {
if !gtk::is_initialized_main_thread() {
log::error!(
"[linux] mount_surface: refused off the GTK main thread (parent={parent}, id={id})"
);
return false;
}
if !crate::widget::runtime::is_mounted(id) {
log::error!(
"[linux] mount_surface: id={id} is not in widget::runtime; \
call runtime::register before mounting"
);
return false;
}
let area = gtk::DrawingArea::new();
area.set_size_request(1, 1);
area.set_has_tooltip(false);
area.connect_draw(move |widget, context| {
let width = widget.allocated_width().max(1) as u32;
let height = widget.allocated_height().max(1) as u32;
match crate::widget::runtime::render_frame_cached(
id,
Size::new(width, height),
Color::WHITE,
) {
Some(frame) => {
blit_rgba(context, width, height, &frame);
}
None => {
log::error!(
"[linux] canvas: widget id={id} produced no frame \
(unmounted, or it does not implement Draw)"
);
}
}
glib::Propagation::Proceed
});
let press_area = area.clone();
area.add_events(
gdk::EventMask::BUTTON_PRESS_MASK
| gdk::EventMask::BUTTON_RELEASE_MASK
| gdk::EventMask::POINTER_MOTION_MASK
| gdk::EventMask::KEY_PRESS_MASK
| gdk::EventMask::SCROLL_MASK
| gdk::EventMask::TOUCH_MASK,
);
area.set_can_focus(true);
let ime = ImeContext::new(&area);
let origin = Point::new(rect.x, rect.y);
let double_click = Rc::new(RefCell::new(DoubleClickTracker::new()));
area.connect_button_press_event(move |area, event| {
let position = Point::new(event.position().0 as i32, event.position().1 as i32);
let absolute = Point::new(origin.x + position.x, origin.y + position.y);
let is_double = event.button() == 1
&& double_click.borrow_mut().register(
event.time(),
event.position().0,
event.position().1,
);
let translated = if is_double {
Event::mouse_double_click(absolute.x, absolute.y, 1)
} else {
Event::mouse_press_with(absolute.x, absolute.y, 1, modifier_state_bits(event.state()))
};
let delivered = forward_pointer_to_platform(id, &translated, absolute);
if delivered {
focus_area_if_enabled(area);
press_area.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_button_release_event(move |widget, event| {
let position = Point::new(event.position().0 as i32, event.position().1 as i32);
let absolute = Point::new(origin.x + position.x, origin.y + position.y);
if forward_pointer_to_platform(
id,
&Event::MouseRelease { pos: absolute, button: 1 },
absolute,
) {
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_motion_notify_event(move |widget, event| {
let position = Point::new(event.position().0 as i32, event.position().1 as i32);
let absolute = Point::new(origin.x + position.x, origin.y + position.y);
let hover_before = if pointer_button_down(event.state()) {
None
} else {
Some(crate::widget::runtime::hovered_widget())
};
if forward_pointer_to_platform(id, &Event::MouseMove { pos: absolute }, absolute)
&& hover_before != Some(crate::widget::runtime::hovered_widget())
{
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_leave_notify_event(move |widget, _| {
crate::widget::runtime::clear_hover(Point::new(0, 0));
widget.queue_draw();
note_canvas_redraw(id);
glib::Propagation::Proceed
});
#[cfg(feature = "touch")]
area.connect_touch_event(move |widget, event| {
let Some(touch) = event.downcast_ref::<gdk::EventTouch>() else {
return glib::Propagation::Proceed;
};
let translated = match event.event_type() {
gdk::EventType::TouchBegin => TouchPhase::Begin,
gdk::EventType::TouchUpdate => TouchPhase::Update,
gdk::EventType::TouchCancel => TouchPhase::Cancel,
gdk::EventType::TouchEnd => TouchPhase::End,
_ => return glib::Propagation::Proceed,
};
let position = Point::new(touch.position().0 as i32, touch.position().1 as i32);
let absolute = Point::new(origin.x + position.x, origin.y + position.y);
let touch_id = touch.event_sequence().map(|sequence| sequence.as_ptr() as u64).unwrap_or(0);
let widget_event = match translated {
TouchPhase::Begin => Event::TouchBegin { pos: absolute, touch_id },
TouchPhase::Update => Event::TouchMove { pos: absolute, touch_id },
TouchPhase::End => Event::TouchEnd { pos: absolute, touch_id },
TouchPhase::Cancel => crate::event::translator::touch_cancel(absolute, touch_id),
};
if forward_pointer_to_platform(id, &widget_event, absolute) {
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_key_press_event(move |widget, event| {
if gdk_keyval_is_tab(*event.keyval()) {
const WIDGET_SHIFT: u32 = 1;
let forward = modifier_bits(event) & WIDGET_SHIFT == 0;
crate::widget::runtime::focus_next(forward);
widget.queue_draw();
note_canvas_redraw(id);
return glib::Propagation::Stop;
}
if ime.borrow().as_ref().is_some_and(|ime| ime.filter_keypress(event)) {
widget.queue_draw();
note_canvas_redraw(id);
return glib::Propagation::Stop;
}
let translated = if let Some(text) = printable_text(event) {
Event::TextInput { text }
} else {
let modifiers = modifier_bits(event);
let key = logical_key(*event.keyval(), is_control_chord(modifiers));
Event::KeyPress { key, modifiers }
};
if forward_key_to_platform(id, &translated) {
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_key_release_event(move |widget, event| {
let modifiers = modifier_bits(event);
let key = logical_key(*event.keyval(), is_control_chord(modifiers));
let released = Event::KeyRelease { key, modifiers };
if forward_key_to_platform(id, &released) {
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
area.connect_scroll_event(move |widget, event| {
let (_, delta_y) = event.delta();
let dy = match event.direction() {
gdk::ScrollDirection::Up => -1.0,
gdk::ScrollDirection::Down => 1.0,
_ => delta_y,
};
if crate::widget::runtime::dispatch_event(
id,
&Event::Wheel { delta: Point::new(0, dy.round() as i32), modifiers: 0 },
) {
widget.queue_draw();
note_canvas_redraw(id);
}
glib::Propagation::Proceed
});
let mut native = platform.native.lock_guard();
let placed = if let Some(overlay) = native.content_overlay.get(&parent) {
super::overlay_place::place(overlay, &area, rect.x, rect.y, rect.width, rect.height);
true
} else {
log::error!("[linux] mount_surface: window {parent} has no content container");
false
};
if placed {
native.widgets.insert(id, area.clone().upcast::<gtk::Widget>());
native.canvases.insert(id, area.clone());
area.show();
}
drop(native);
if placed {
crate::widget::runtime::set_geometry(id, rect);
area.queue_draw();
note_canvas_redraw(id);
}
placed
}
fn paint_area_for(native: &LinuxNativeState, id: ObjectId) -> Option<>k::DrawingArea> {
if let Some(area) = native.canvases.get(&id) {
return Some(area);
}
let window = crate::widget::runtime::top_level_window_of(id).unwrap_or(id);
let host = crate::widget::runtime::host_window_for(window).unwrap_or(window);
native.window_painters.get(&host)
}
pub(crate) fn repaint_canvas(platform: &LinuxPlatform, id: ObjectId) -> bool {
let native = platform.native.lock_guard();
let Some(area) = paint_area_for(&native, id) else {
return false;
};
area.queue_draw();
true
}
pub(crate) fn note_canvas_redraw(id: ObjectId) {
crate::notify_native_redraw(id, None);
}
pub(crate) fn repaint_canvas_rect(platform: &LinuxPlatform, id: ObjectId, rect: Rect) -> bool {
let native = platform.native.lock_guard();
let Some(area) = paint_area_for(&native, id) else {
return false;
};
let allocation = area.allocation();
let local =
Rect::new(rect.x - allocation.x(), rect.y - allocation.y(), rect.width, rect.height);
if local.width == 0 || local.height == 0 {
return false;
}
if local.x >= allocation.width()
|| local.y >= allocation.height()
|| local.x + local.width as i32 <= 0
|| local.y + local.height as i32 <= 0
{
return false;
}
area.queue_draw_area(local.x, local.y, local.width as i32, local.height as i32);
true
}
pub(crate) fn resize_canvas(platform: &LinuxPlatform, id: ObjectId, rect: Rect) -> bool {
let native = platform.native.lock_guard();
let Some(area) = native.canvases.get(&id) else {
log::error!("[linux] resize_surface: id={id} is not mounted");
return false;
};
super::overlay_place::reposition(area, rect.x, rect.y, rect.width, rect.height);
drop(native);
crate::widget::runtime::set_geometry(id, rect);
true
}
pub(crate) fn unmount_canvas(platform: &LinuxPlatform, id: ObjectId) -> bool {
let mut native = platform.native.lock_guard();
let Some(area) = native.canvases.remove(&id) else {
log::error!("[linux] unmount_surface: id={id} is not mounted");
return false;
};
native.widgets.remove(&id);
unsafe {
area.destroy();
}
true
}
pub(crate) fn blit_rgba(context: &cairo::Context, width: u32, height: u32, frame: &[u8]) {
let stride = cairo::Format::Rgb24.stride_for_width(width);
let Ok(stride) = stride else {
log::error!("[linux] canvas: cairo rejected a {width}-pixel stride");
return;
};
let expected = width as usize * height as usize * 4;
if frame.len() < expected {
log::error!("[linux] canvas: frame is {} bytes, need {expected}", frame.len());
return;
}
let mut buffer = vec![0u8; (stride * height as i32) as usize];
for y in 0..height as usize {
let row = y * stride as usize;
for x in 0..width as usize {
let source = (y * width as usize + x) * 4;
let target = row + x * 4;
buffer[target] = frame[source + 2];
buffer[target + 1] = frame[source + 1];
buffer[target + 2] = frame[source];
buffer[target + 3] = 255;
}
}
let surface = match cairo::ImageSurface::create_for_data(
buffer,
cairo::Format::Rgb24,
width as i32,
height as i32,
stride,
) {
Ok(surface) => surface,
Err(error) => {
log::error!("[linux] canvas: cairo could not wrap the frame: {error}");
return;
}
};
let _ = context.set_source_surface(&surface, 0.0, 0.0);
context.paint().ok();
surface.finish();
}
pub(crate) fn printable_text(event: &gdk::EventKey) -> Option<String> {
let state = event.state();
if state.intersects(
gdk::ModifierType::CONTROL_MASK
| gdk::ModifierType::MOD1_MASK
| gdk::ModifierType::SUPER_MASK,
) {
return None;
}
let ch = event.keyval().to_unicode()?;
if ch.is_control() {
return None;
}
Some(ch.to_string())
}
pub(crate) fn modifier_bits(event: &gdk::EventKey) -> u32 {
modifier_state_bits(event.state())
}
pub(crate) fn modifier_state_bits(state: gdk::ModifierType) -> u32 {
const WIDGET_SHIFT: u32 = 1;
const WIDGET_CONTROL: u32 = 2;
const WIDGET_ALT: u32 = 4;
const WIDGET_META: u32 = 8;
let mut bits = 0u32;
if state.contains(gdk::ModifierType::SHIFT_MASK) {
bits |= WIDGET_SHIFT;
}
if state.contains(gdk::ModifierType::CONTROL_MASK) {
bits |= WIDGET_CONTROL;
}
if state.contains(gdk::ModifierType::MOD1_MASK) {
bits |= WIDGET_ALT;
}
if state.contains(gdk::ModifierType::SUPER_MASK) {
bits |= WIDGET_META;
}
bits
}
pub(crate) fn pointer_button_down(state: gdk::ModifierType) -> bool {
state.intersects(
gdk::ModifierType::BUTTON1_MASK
| gdk::ModifierType::BUTTON2_MASK
| gdk::ModifierType::BUTTON3_MASK
| gdk::ModifierType::BUTTON4_MASK
| gdk::ModifierType::BUTTON5_MASK,
)
}
pub(crate) fn gdk_keyval_is_tab(keyval: u32) -> bool {
keyval == KEY_TAB
}
pub(crate) fn is_control_chord(modifiers: u32) -> bool {
const WIDGET_CONTROL: u32 = 2;
const WIDGET_ALT: u32 = 4;
const WIDGET_META: u32 = 8;
modifiers & (WIDGET_CONTROL | WIDGET_ALT | WIDGET_META) != 0
}
pub(crate) fn logical_key(keyval: u32, control_chord: bool) -> u32 {
use crate::event::key_codes as keys;
let named = match keyval {
KEY_RETURN | KEY_KP_ENTER => keys::ENTER,
KEY_ESCAPE => keys::ESCAPE,
KEY_BACKSPACE => keys::BACKSPACE,
KEY_DELETE => keys::DELETE,
KEY_LEFT => keys::LEFT,
KEY_UP => keys::UP,
KEY_RIGHT => keys::RIGHT,
KEY_DOWN => keys::DOWN,
KEY_HOME => keys::HOME,
KEY_END => keys::END,
KEY_PAGE_UP => keys::PAGE_UP,
KEY_PAGE_DOWN => keys::PAGE_DOWN,
KEY_TAB => keys::TAB,
KEY_F1 => keys::F1,
KEY_F2 => keys::F2,
KEY_F3 => keys::F3,
KEY_F4 => keys::F4,
KEY_F5 => keys::F5,
KEY_F6 => keys::F6,
KEY_F7 => keys::F7,
KEY_F8 => keys::F8,
KEY_F9 => keys::F9,
KEY_F10 => keys::F10,
KEY_F11 => keys::F11,
KEY_F12 => keys::F12,
_ => 0,
};
if named != 0 {
return named;
}
if control_chord {
if let Some(ch) = char::from_u32(keyval) {
if let Some(code) = keys::ascii_key_code(ch) {
return code;
}
}
}
keyval
}
const KEY_RETURN: u32 = 0xFF0D;
const KEY_KP_ENTER: u32 = 0xFF8D;
const KEY_ESCAPE: u32 = 0xFF1B;
const KEY_BACKSPACE: u32 = 0xFF08;
const KEY_DELETE: u32 = 0xFFFF;
const KEY_LEFT: u32 = 0xFF51;
const KEY_UP: u32 = 0xFF52;
const KEY_RIGHT: u32 = 0xFF53;
const KEY_DOWN: u32 = 0xFF54;
const KEY_HOME: u32 = 0xFF50;
const KEY_END: u32 = 0xFF57;
const KEY_PAGE_UP: u32 = 0xFF55;
const KEY_PAGE_DOWN: u32 = 0xFF56;
const KEY_F1: u32 = 0xFFBE;
const KEY_F2: u32 = 0xFFBF;
const KEY_F3: u32 = 0xFFC0;
const KEY_F4: u32 = 0xFFC1;
const KEY_F5: u32 = 0xFFC2;
const KEY_F6: u32 = 0xFFC3;
const KEY_F7: u32 = 0xFFC4;
const KEY_F8: u32 = 0xFFC5;
const KEY_F9: u32 = 0xFFC6;
const KEY_F10: u32 = 0xFFC7;
const KEY_F11: u32 = 0xFFC8;
const KEY_F12: u32 = 0xFFC9;
const KEY_TAB: u32 = 0xFF09;
pub(crate) struct DoubleClickTracker {
last: Option<(u32, f64, f64)>,
interval_ms: u32,
distance_px: f64,
}
impl DoubleClickTracker {
pub(crate) fn new() -> Self {
let settings = gtk::Settings::default();
let interval_ms = settings
.as_ref()
.map(|settings| settings.gtk_double_click_time().max(0) as u32)
.unwrap_or(400);
let distance_px = settings
.as_ref()
.map(|settings| settings.gtk_double_click_distance().max(0) as f64)
.unwrap_or(5.0);
Self { last: None, interval_ms, distance_px }
}
pub(crate) fn register(&mut self, time_ms: u32, x: f64, y: f64) -> bool {
let is_double = self.last.is_some_and(|(last_time, last_x, last_y)| {
let within_time = time_ms.saturating_sub(last_time) <= self.interval_ms;
let dx = x - last_x;
let dy = y - last_y;
within_time && dx * dx + dy * dy <= self.distance_px * self.distance_px
});
self.last = if is_double { None } else { Some((time_ms, x, y)) };
is_double
}
}
#[cfg(feature = "touch")]
#[derive(Clone, Copy)]
enum TouchPhase {
Begin,
Update,
End,
Cancel,
}
fn forward_pointer_to_platform(id: ObjectId, event: &Event, absolute: Point) -> bool {
crate::platform::platform_facts().route_pointer_event(id, event, absolute)
}
fn forward_key_to_platform(id: ObjectId, event: &Event) -> bool {
match crate::widget::runtime::focused_widget() {
Some(focused) => crate::widget::runtime::dispatch_event(focused, event),
None => crate::widget::runtime::dispatch_event(id, event),
}
}
pub(crate) fn focus_area_if_enabled(area: >k::DrawingArea) -> bool {
if !area.is_sensitive() {
return false;
}
area.grab_focus();
true
}
pub(crate) struct ImeContext {
context: gtk::IMMulticontext,
last_focused: std::rc::Rc<std::cell::RefCell<Option<ObjectId>>>,
}
impl ImeContext {
pub(crate) fn new(area: >k::DrawingArea) -> std::rc::Rc<std::cell::RefCell<Option<Self>>> {
let this = std::rc::Rc::new(std::cell::RefCell::new(Some(Self {
context: gtk::IMMulticontext::new(),
last_focused: std::rc::Rc::new(std::cell::RefCell::new(None)),
})));
let weak = std::rc::Rc::downgrade(&this);
{
let borrowed = this.borrow();
let context = &borrowed.as_ref().expect("constructed just above").context;
context.set_use_preedit(true);
if let Some(window) = area.window() {
context.set_client_window(Some(&window));
}
context.connect_preedit_changed(|context| {
let (text, _, cursor) = context.preedit_string();
let Some(widget) = crate::widget::runtime::focused_widget() else {
return;
};
if !control_accepts_ime(widget) {
return;
}
let composition = crate::platform::ime::ImeComposition {
text: text.to_string(),
cursor_position: cursor.max(0) as usize,
selection_length: 0,
};
let _ = crate::platform::ime::deliver_composition(widget, &composition);
});
context.connect_preedit_end(|_| {
let Some(widget) = crate::widget::runtime::focused_widget() else {
return;
};
if !control_accepts_ime(widget) {
return;
}
let _ = crate::platform::ime::deliver_composition(
widget,
&crate::platform::ime::ImeComposition::default(),
);
});
context.connect_commit(|_, text| {
let Some(widget) = crate::widget::runtime::focused_widget() else {
return;
};
if !control_accepts_ime(widget) {
return;
}
let _ = crate::platform::ime::deliver_composition(
widget,
&crate::platform::ime::ImeComposition::default(),
);
let _ = crate::platform::ime::deliver_commit(widget, text);
});
}
let mapped = weak.clone();
area.connect_map(move |area| {
if let Some(this) = mapped.upgrade() {
if let Some(ime) = this.borrow().as_ref() {
if let Some(window) = area.window() {
ime.context.set_client_window(Some(&window));
}
}
}
});
let focus_in = weak.clone();
area.connect_focus_in_event(move |_, _| {
if let Some(this) = focus_in.upgrade() {
if let Some(ime) = this.borrow().as_ref() {
ime.context.focus_in();
}
}
glib::Propagation::Proceed
});
area.connect_focus_out_event(move |_, _| {
if let Some(this) = weak.upgrade() {
if let Some(ime) = this.borrow().as_ref() {
ime.context.focus_out();
}
}
glib::Propagation::Proceed
});
this
}
fn sync_focus(&self) -> Option<ObjectId> {
let focused = crate::widget::runtime::focused_widget();
let mut last = self.last_focused.borrow_mut();
match focused {
Some(id) => {
if *last != Some(id) {
if last.is_some() {
self.context.focus_out();
}
self.context.focus_in();
*last = Some(id);
}
Some(id)
}
None => {
if last.is_some() {
self.context.focus_out();
*last = None;
}
None
}
}
}
pub(crate) fn filter_keypress(&self, event: &gdk::EventKey) -> bool {
let Some(id) = self.sync_focus() else {
return false;
};
if !control_accepts_ime(id) {
return false;
}
if let Some(rect) = crate::widget::runtime::geometry_of(id) {
self.context.set_cursor_location(&gdk::Rectangle::new(
rect.x,
rect.y,
rect.width as i32,
rect.height as i32,
));
}
self.context.filter_keypress(event)
}
}
impl ImeContext {
pub(crate) fn release(&self) {
if gtk::is_initialized_main_thread() {
self.context.focus_out();
self.context.reset();
}
}
}
pub(crate) fn control_accepts_ime(id: ObjectId) -> bool {
crate::widget::runtime::accepts_ime_input(id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modifier_bits_map_shift_control_and_alt() {
assert!(gdk::ModifierType::SHIFT_MASK.bits() != 0);
assert!(gdk::ModifierType::CONTROL_MASK.bits() != 0);
assert!(gdk::ModifierType::MOD1_MASK.bits() != 0);
}
#[test]
fn motion_with_a_held_button_is_a_drag() {
assert!(!pointer_button_down(gdk::ModifierType::SHIFT_MASK));
for button in [
gdk::ModifierType::BUTTON1_MASK,
gdk::ModifierType::BUTTON2_MASK,
gdk::ModifierType::BUTTON3_MASK,
gdk::ModifierType::BUTTON4_MASK,
gdk::ModifierType::BUTTON5_MASK,
] {
assert!(pointer_button_down(button));
assert!(pointer_button_down(button | gdk::ModifierType::CONTROL_MASK));
}
}
#[test]
fn blit_rejects_short_frames_without_panicking() {
let short = [0u8; 4];
assert!(short.len() < 4 * 4 * 4);
}
}