#![cfg(all(
target_os = "linux",
feature = "gtk-native",
not(any(feature = "mini", feature = "embedded"))
))]
use super::types::LinuxPlatform;
use crate::core::{Color, ObjectId, Point, Rect, Size};
use crate::event::Event;
use gtk::prelude::*;
pub(crate) fn mount_canvas(
platform: &LinuxPlatform,
parent: ObjectId,
id: ObjectId,
rect: Rect,
) -> bool {
if !crate::widget::runtime::is_mounted(id) {
log::error!(
"[linux] mount_self_drawn: id={id} is not in widget::runtime; \
call runtime::register before mounting"
);
return false;
}
let area = gtk::DrawingArea::new();
area.set_size_request(rect.width as i32, rect.height as i32);
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(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,
);
area.set_can_focus(true);
area.connect_button_press_event(move |_, event| {
let position = Point::new(event.position().0 as i32, event.position().1 as i32);
let delivered = crate::widget::runtime::dispatch_event(
id,
&Event::MousePress { pos: position, button: 1 },
);
if delivered {
press_area.queue_draw();
}
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);
if crate::widget::runtime::dispatch_event(
id,
&Event::MouseRelease { pos: position, button: 1 },
) {
widget.queue_draw();
}
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);
if crate::widget::runtime::dispatch_event(id, &Event::MouseMove { pos: position }) {
widget.queue_draw();
}
glib::Propagation::Proceed
});
area.connect_key_press_event(move |widget, event| {
let translated = if let Some(text) = printable_text(event) {
Event::TextInput { text }
} else {
let key = *event.keyval();
Event::KeyPress { key, modifiers: modifier_bits(event) }
};
if crate::widget::runtime::dispatch_event(id, &translated) {
widget.queue_draw();
}
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();
}
glib::Propagation::Proceed
});
let mut native = platform.native.lock_guard();
let placed = if let Some(container) = native.content_fixed.get(&parent) {
container.put(&area, rect.x, rect.y);
true
} else {
log::error!("[linux] mount_self_drawn: window {parent} has no content container");
false
};
if placed {
native.widgets.insert(id, area.clone().upcast::<gtk::Widget>());
native.canvases.insert(id, area.clone());
if let Some(window) = native.windows.get(&parent) {
window.show_all();
}
}
drop(native);
if placed {
crate::widget::runtime::set_geometry(id, rect);
area.queue_draw();
}
placed
}
pub(crate) fn repaint_canvas(platform: &LinuxPlatform, id: ObjectId) -> bool {
let native = platform.native.lock_guard();
let Some(area) = native.canvases.get(&id) else {
return false;
};
area.queue_draw();
true
}
pub(crate) fn resize_canvas(platform: &LinuxPlatform, id: ObjectId, rect: Rect) -> bool {
let mut native = platform.native.lock_guard();
let Some(area) = native.canvases.get(&id) else {
log::error!("[linux] resize_self_drawn: id={id} is not mounted");
return false;
};
area.set_size_request(rect.width as i32, rect.height as i32);
area.move_(rect.x, rect.y);
area.queue_resize();
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_self_drawn: id={id} is not mounted");
return false;
};
native.widgets.remove(&id);
area.destroy();
true
}
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();
}
fn printable_text(event: &gdk::EventKey) -> Option<String> {
let ch = event.keyval().to_unicode()?;
if ch.is_control() {
return None;
}
Some(ch.to_string())
}
fn modifier_bits(event: &gdk::EventKey) -> u32 {
let state = event.state();
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modifier_bits_map_shift_control_and_alt() {
assert_eq!(gdk::ModifierType::SHIFT_MASK.bits() != 0, true);
assert_eq!(gdk::ModifierType::CONTROL_MASK.bits() != 0, true);
assert_eq!(gdk::ModifierType::MOD1_MASK.bits() != 0, true);
}
#[test]
fn blit_rejects_short_frames_without_panicking() {
let short = [0u8; 4];
assert!(short.len() < 4 * 4 * 4);
}
}