use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use gtk::gdk::prelude::{GdkCairoContextExt, PaintableExt, TextureExt};
use gtk::glib::translate::ToGlibPtr;
use gtk::gsk::prelude::GskRendererExt;
use gtk::prelude::*;
use gtk4 as gtk;
use crate::native_draw_support::{NativeDrawPalette, NativeDrawTextStyleResolver};
use crate::{
Color, HorizontalAlign, NativeDrawCommand, NativeDrawCommandSink, NativeDrawIconCommand,
NativeDrawImageCommand, NativeDrawPlan, NativeDrawTextCommand, NativeIconColorMode,
NativeImageInterpolation, NativeStyleResolver, Rect, Size, TextLayout, TextRun, TextStyle,
TextWeight, TextWrap, VerticalAlign,
};
#[link(name = "pangocairo-1.0")]
unsafe extern "C" {
fn pango_cairo_show_layout(
context: *mut gtk::cairo::ffi::cairo_t,
layout: *mut gtk::pango::ffi::PangoLayout,
);
}
pub(crate) fn install_linux_gtk_draw_plan(
window: >k::ApplicationWindow,
plan: NativeDrawPlan,
mut runtime: crate::native::NativeViewInputRuntime,
) -> LinuxGtkDrawViewHost {
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
let drawing_area = gtk::DrawingArea::builder()
.accessible_role(gtk::AccessibleRole::TextBox)
.build();
#[cfg(not(all(feature = "accessibility", feature = "text-input-core")))]
let drawing_area = gtk::DrawingArea::new();
drawing_area.set_hexpand(true);
drawing_area.set_vexpand(true);
drawing_area.set_focusable(true);
let mut plan = plan;
if let Some(updated) = runtime.set_typography_scale(linux_gtk_ui_font_scale()) {
plan = updated;
}
let plan = Rc::new(RefCell::new(plan));
#[cfg(feature = "text-input-core")]
runtime.set_text_shaping_backend(linux_gtk_text_shaping_backend(drawing_area.pango_context()));
runtime.defer_app_command_execution();
runtime.defer_ui_command_execution();
let runtime = Rc::new(RefCell::new(runtime));
window.connect_destroy({
let runtime = Rc::clone(&runtime);
move |_| runtime.borrow_mut().shutdown_dispatch()
});
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
sync_linux_gtk_text_accessibility(&drawing_area, &runtime);
let runtime_timer = Rc::new(RefCell::new(None));
let application = window.application();
let ime = gtk::IMMulticontext::new();
ime.set_client_widget(Some(&drawing_area));
ime.set_use_preedit(true);
drawing_area.set_draw_func({
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let application = application.clone();
let runtime_timer = Rc::clone(&runtime_timer);
let ime = ime.clone();
move |area, context, width, height| {
let invalidation = runtime.borrow_mut().refresh_invalidated_view();
if invalidation.handled || invalidation.redraw_plan.is_some() {
apply_linux_gtk_input_report_during_draw(
invalidation,
area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
schedule_linux_gtk_runtime_tick(
area,
&plan,
&runtime,
&ime,
application.clone(),
&runtime_timer,
);
}
let resize = runtime.borrow_mut().set_surface(
Rect {
x: 0,
y: 0,
width: width.max(0),
height: height.max(0),
},
crate::Dpi::standard(),
);
if let Some(updated) = resize.redraw_plan {
*plan.borrow_mut() = updated;
}
if resize.surface_changed {
sync_linux_gtk_ime(area, &runtime, &ime);
}
let plan = plan.borrow();
let (system_prefers_dark, system_high_contrast) = linux_gtk_system_appearance();
let palette = NativeDrawPalette::for_system_appearance(
plan.theme_mode,
system_prefers_dark,
system_high_contrast,
linux_gtk_semantic_palette(area),
system_high_contrast
.then(|| linux_gtk_semantic_high_contrast_palette(area))
.flatten(),
);
let mut sink = LinuxGtkDrawSink::new(area, context, palette, plan.typography_scale());
sink.set_source(palette.surface);
let _ = context.paint();
sink.draw_plan(&plan);
}
});
if let Some(settings) = gtk::Settings::default() {
let area = drawing_area.downgrade();
let runtime = Rc::clone(&runtime);
let plan = Rc::clone(&plan);
settings.connect_gtk_font_name_notify(move |_settings| {
if let Some(area) = area.upgrade() {
if let Some(updated) = runtime
.borrow_mut()
.set_typography_scale(linux_gtk_ui_font_scale())
{
*plan.borrow_mut() = updated;
}
area.queue_resize();
area.queue_draw();
}
});
let area = drawing_area.downgrade();
settings.connect_gtk_theme_name_notify(move |_settings| {
if let Some(area) = area.upgrade() {
area.queue_draw();
}
});
let area = drawing_area.downgrade();
settings.connect_gtk_application_prefer_dark_theme_notify(move |_settings| {
if let Some(area) = area.upgrade() {
area.queue_draw();
}
});
}
let gesture = gtk::GestureClick::new();
gesture.set_button(0);
gesture.connect_pressed({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |gesture, _press_count, x, y| {
#[cfg(feature = "window-chrome")]
if gesture.current_button() == 1
&& begin_linux_gtk_window_gesture(gesture, &area, &runtime, x, y)
{
gesture.set_state(gtk::EventSequenceState::Claimed);
return;
}
let report = runtime.borrow_mut().dispatch_pointer_down_with_button(
crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
},
linux_gtk_pointer_button(gesture.current_button()),
linux_gtk_pointer_modifiers(gesture.current_event_state()),
);
if report.handled {
area.grab_focus();
}
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
});
gesture.connect_released({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |gesture, _press_count, x, y| {
let report = runtime.borrow_mut().dispatch_pointer_up_with_button(
crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
},
linux_gtk_pointer_button(gesture.current_button()),
linux_gtk_pointer_modifiers(gesture.current_event_state()),
);
if report.handled {
area.grab_focus();
}
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
reset_linux_gtk_ime_if_no_text_target(&runtime, &ime);
}
});
gesture.connect_cancel({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |_gesture, _sequence| {
let report = runtime.borrow_mut().cancel_pointer_drag();
if report.handled {
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
}
});
drawing_area.add_controller(gesture);
let motion = gtk::EventControllerMotion::new();
motion.connect_motion({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
let runtime_timer = Rc::clone(&runtime_timer);
move |motion, x, y| {
let report = runtime.borrow_mut().dispatch_pointer_move_with_modifiers(
crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
},
linux_gtk_pointer_modifiers(motion.current_event_state()),
);
#[cfg(feature = "resize")]
area.set_cursor_from_name(Some(
match runtime.borrow().resize_cursor_at(crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
}) {
Some(crate::ZsResizeAxis::Horizontal) => "col-resize",
Some(crate::ZsResizeAxis::Vertical) => "row-resize",
None => "default",
},
));
if report.handled {
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
schedule_linux_gtk_runtime_tick(
&area,
&plan,
&runtime,
&ime,
application.clone(),
&runtime_timer,
);
}
});
motion.connect_leave({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
let runtime_timer = Rc::clone(&runtime_timer);
move |_motion| {
let report = runtime.borrow_mut().dispatch_pointer_leave();
if report.handled {
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
schedule_linux_gtk_runtime_tick(
&area,
&plan,
&runtime,
&ime,
application.clone(),
&runtime_timer,
);
}
});
drawing_area.add_controller(motion);
let scroll = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
scroll.connect_scroll({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
move |controller, _delta_x, delta_y| {
if delta_y.abs() < f64::EPSILON {
return gtk::glib::Propagation::Proceed;
}
let (x, y) = controller
.current_event()
.and_then(|event| event.position())
.unwrap_or((0.0, 0.0));
let report = runtime.borrow_mut().dispatch_pointer_scroll(
crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
},
crate::Dp::new((delta_y * 48.0) as f32),
);
if let Some(updated) = report.redraw_plan {
*plan.borrow_mut() = updated;
area.queue_draw();
}
if report.quit_requested {
if let Some(application) = &application {
application.quit();
}
}
if report.handled {
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
}
});
drawing_area.add_controller(scroll);
ime.connect_preedit_changed({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |context| {
let (text, _attributes, cursor) = context.preedit_string();
let report = dispatch_linux_gtk_ime_preedit(
&runtime,
text.as_str(),
(!text.is_empty()).then_some(cursor.max(0) as usize),
);
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
});
ime.connect_commit({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |_context, text| {
let report = dispatch_linux_gtk_ime_commit(&runtime, text);
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
reset_linux_gtk_ime_if_no_text_target(&runtime, &ime);
}
});
let keyboard = gtk::EventControllerKey::new();
keyboard.set_im_context(Some(&ime));
keyboard.connect_key_pressed({
let application = window.application();
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
let runtime_timer = Rc::clone(&runtime_timer);
move |_controller, key, _keycode, modifiers| {
let shift = modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK);
let control = modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK);
#[cfg(feature = "text-input-core")]
let alt = modifiers.contains(gtk::gdk::ModifierType::ALT_MASK);
#[cfg(feature = "text-input-core")]
let super_or_meta = modifiers
.intersects(gtk::gdk::ModifierType::SUPER_MASK | gtk::gdk::ModifierType::META_MASK);
let command_or_control = modifiers.intersects(
gtk::gdk::ModifierType::CONTROL_MASK
| gtk::gdk::ModifierType::SUPER_MASK
| gtk::gdk::ModifierType::META_MASK,
);
#[cfg(feature = "text-input-core")]
let text_edit_shortcut = control && !shift && !alt && !super_or_meta;
let mut runtime_state = runtime.borrow_mut();
let report = match key {
gtk::gdk::Key::Tab => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Tab,
shift,
control,
),
gtk::gdk::Key::ISO_Left_Tab => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Tab,
true,
control,
),
gtk::gdk::Key::Return | gtk::gdk::Key::KP_Enter => {
let report = runtime_state.dispatch_key(crate::NativeViewKey::Enter);
if report.handled {
report
} else {
runtime_state.dispatch_text_input("\r")
}
}
gtk::gdk::Key::space => {
let report = runtime_state.dispatch_key(crate::NativeViewKey::Space);
if report.handled || command_or_control {
report
} else {
runtime_state.dispatch_text_input(" ")
}
}
gtk::gdk::Key::Up => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Up,
shift,
control,
),
gtk::gdk::Key::Down => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Down,
shift,
control,
),
gtk::gdk::Key::Left => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Left,
shift,
control,
),
gtk::gdk::Key::Right => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::Right,
shift,
control,
),
gtk::gdk::Key::Home => {
runtime_state.dispatch_key_with_shift(crate::NativeViewKey::Home, shift)
}
gtk::gdk::Key::End => {
runtime_state.dispatch_key_with_shift(crate::NativeViewKey::End, shift)
}
gtk::gdk::Key::Page_Up => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::PageUp,
shift,
control,
),
gtk::gdk::Key::Page_Down => runtime_state.dispatch_key_with_modifiers(
crate::NativeViewKey::PageDown,
shift,
control,
),
gtk::gdk::Key::BackSpace => runtime_state.dispatch_text_input("\u{8}"),
gtk::gdk::Key::Delete => runtime_state.dispatch_text_input("\u{7f}"),
#[cfg(feature = "text-input-core")]
_ if text_edit_shortcut => key
.to_unicode()
.and_then(crate::native_text_edit::text_edit_command_for_shortcut_character)
.map(|command| runtime_state.dispatch_text_edit_shortcut(command))
.unwrap_or_default(),
_ if !command_or_control => key
.to_unicode()
.filter(|character| !character.is_control())
.map(|character| runtime_state.dispatch_text_input(&character.to_string()))
.unwrap_or_default(),
_ => crate::native::NativeViewInputDispatchReport::default(),
};
drop(runtime_state);
let handled = report.handled;
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
reset_linux_gtk_ime_if_no_text_target(&runtime, &ime);
schedule_linux_gtk_runtime_tick(
&area,
&plan,
&runtime,
&ime,
application.clone(),
&runtime_timer,
);
if handled {
gtk::glib::Propagation::Stop
} else {
gtk::glib::Propagation::Proceed
}
}
});
drawing_area.add_controller(keyboard);
let focus = gtk::EventControllerFocus::new();
focus.connect_enter({
let area = drawing_area.clone();
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |_focus| sync_linux_gtk_ime(&area, &runtime, &ime)
});
focus.connect_leave({
let area = drawing_area.clone();
let plan = Rc::clone(&plan);
let runtime = Rc::clone(&runtime);
let ime = ime.clone();
move |_focus| {
let report = runtime.borrow_mut().blur_focus();
if let Some(updated) = report.redraw_plan {
*plan.borrow_mut() = updated;
area.queue_draw();
}
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
sync_linux_gtk_text_accessibility(&area, &runtime);
ime.reset();
ime.focus_out();
}
});
drawing_area.add_controller(focus);
window.set_child(Some(&drawing_area));
let invalidation_action = gtk::gio::SimpleAction::new("zsui-invalidate", None);
invalidation_action.connect_activate({
let area = drawing_area.clone();
let runtime = runtime.clone();
let plan = plan.clone();
let ime = ime.clone();
let application = application.clone();
let timer = runtime_timer.clone();
move |_, _| {
let report = runtime.borrow_mut().refresh_invalidated_view();
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
schedule_linux_gtk_runtime_tick(
&area,
&plan,
&runtime,
&ime,
application.clone(),
&timer,
);
}
});
let invalidation_binding = {
let action = gtk::glib::SendWeakRef::from(invalidation_action.downgrade());
runtime.borrow().bind_invalidation_target(move || {
let action = action.clone();
gtk::glib::idle_add_once(move || {
if let Some(action) = action.upgrade() {
action.activate(None);
}
});
})
};
schedule_linux_gtk_runtime_tick(
&drawing_area,
&plan,
&runtime,
&ime,
application.clone(),
&runtime_timer,
);
drawing_area.connect_scale_factor_notify({
let plan = plan.clone();
let runtime = runtime.clone();
let ime = ime.clone();
let application = application.clone();
move |area| {
#[cfg(feature = "native-smoke")]
crate::native::record_native_display_scale(
"gtk",
"scale-factor-notify",
f64::from(area.scale_factor()),
);
let report = runtime.borrow_mut().display_scale_changed();
apply_linux_gtk_input_report(report, area, &plan, &runtime, &ime, application.as_ref());
}
});
#[cfg(feature = "window-chrome")]
window.connect_maximized_notify({
let plan = plan.clone();
let runtime = runtime.clone();
let ime = ime.clone();
let application = application.clone();
let area = drawing_area.clone();
move |window| {
let report = runtime
.borrow_mut()
.dispatch_window_state(crate::ZsWindowState {
maximized: window.is_maximized(),
});
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
}
});
let paintable = gtk::WidgetPaintable::new(Some(&drawing_area));
LinuxGtkDrawViewHost {
area: drawing_area,
paintable,
plan,
runtime,
ime,
application,
runtime_timer,
_invalidation_binding: invalidation_binding,
_invalidation_action: invalidation_action,
}
}
#[derive(Clone)]
pub(crate) struct LinuxGtkDrawViewHost {
area: gtk::DrawingArea,
paintable: gtk::WidgetPaintable,
plan: Rc<RefCell<NativeDrawPlan>>,
runtime: Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: gtk::IMMulticontext,
application: Option<gtk::Application>,
runtime_timer: Rc<RefCell<Option<gtk::glib::SourceId>>>,
_invalidation_binding: Option<crate::native::UiInvalidationBinding>,
_invalidation_action: gtk::gio::SimpleAction,
}
impl std::fmt::Debug for LinuxGtkDrawViewHost {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LinuxGtkDrawViewHost")
.finish_non_exhaustive()
}
}
impl LinuxGtkDrawViewHost {
pub(crate) fn surface_size(&self) -> crate::Size {
crate::Size {
width: self.area.width(),
height: self.area.height(),
}
}
pub(crate) fn observe_size_changes(&self, observe: impl Fn(crate::Size) + 'static) {
self.area
.connect_resize(move |_, width, height| observe(crate::Size { width, height }));
}
pub(crate) fn accessibility_node_count(&self) -> usize {
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
{
return usize::from(
self.area.accessible_role() == gtk::AccessibleRole::TextBox
&& self.area.is_visible(),
);
}
#[cfg(not(all(feature = "accessibility", feature = "text-input-core")))]
0
}
pub(crate) fn dispatch_proof_inputs(
&self,
inputs: &[crate::NativeViewSmokeInput],
) -> Vec<crate::native::NativeViewInputDispatchReport> {
let mut reports = Vec::new();
for input in inputs {
let dispatch = |report, reports: &mut Vec<_>| {
reports.push(apply_linux_gtk_input_report(
report,
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.as_ref(),
));
};
match input {
crate::NativeViewSmokeInput::Move(point) => {
let report = self.runtime.borrow_mut().dispatch_pointer_move(*point);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::Click(point) => {
let down = self
.runtime
.borrow_mut()
.dispatch_pointer_down(*point, false);
dispatch(down, &mut reports);
let up = self.runtime.borrow_mut().dispatch_pointer_up(*point);
dispatch(up, &mut reports);
}
crate::NativeViewSmokeInput::ClickWidget(widget) => {
let point = self
.runtime
.borrow()
.native_proof_widget_points(*widget)
.map(|points| points.0);
if let Some(point) = point {
let down = self
.runtime
.borrow_mut()
.dispatch_pointer_down(point, false);
dispatch(down, &mut reports);
let up = self.runtime.borrow_mut().dispatch_pointer_up(point);
dispatch(up, &mut reports);
} else {
reports.push(Default::default());
reports.push(Default::default());
}
}
crate::NativeViewSmokeInput::Drag { start, end } => {
let down = self
.runtime
.borrow_mut()
.dispatch_pointer_down(*start, false);
dispatch(down, &mut reports);
let moved = self.runtime.borrow_mut().dispatch_pointer_move(*end);
dispatch(moved, &mut reports);
let up = self.runtime.borrow_mut().dispatch_pointer_up(*end);
dispatch(up, &mut reports);
}
crate::NativeViewSmokeInput::DragWidget(widget) => {
let points = self.runtime.borrow().native_proof_widget_points(*widget);
if let Some((_, start, end)) = points {
let down = self
.runtime
.borrow_mut()
.dispatch_pointer_down(start, false);
dispatch(down, &mut reports);
let moved = self.runtime.borrow_mut().dispatch_pointer_move(end);
dispatch(moved, &mut reports);
let up = self.runtime.borrow_mut().dispatch_pointer_up(end);
dispatch(up, &mut reports);
} else {
reports.push(Default::default());
reports.push(Default::default());
reports.push(Default::default());
}
}
crate::NativeViewSmokeInput::PointerDrag {
start,
end,
button,
modifiers,
} => {
let down = self
.runtime
.borrow_mut()
.dispatch_pointer_down_with_button(*start, *button, *modifiers);
dispatch(down, &mut reports);
let moved = self
.runtime
.borrow_mut()
.dispatch_pointer_move_with_modifiers(*end, *modifiers);
dispatch(moved, &mut reports);
let up = self
.runtime
.borrow_mut()
.dispatch_pointer_up_with_button(*end, *button, *modifiers);
dispatch(up, &mut reports);
}
crate::NativeViewSmokeInput::Text(text) => {
let report = self.runtime.borrow_mut().dispatch_ime_commit(text);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::ImePreedit { text, selection } => {
let report = dispatch_linux_gtk_ime_preedit(
&self.runtime,
text,
selection.map(|(_, caret)| caret),
);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::ImeCommit(text) => {
let report = dispatch_linux_gtk_ime_commit(&self.runtime, text);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::ImeCancel => {
let report = dispatch_linux_gtk_ime_cancel(&self.runtime);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::KeyDown(key) => {
let report = self.runtime.borrow_mut().dispatch_key(*key);
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::Scroll { point, delta_y } => {
let report = self
.runtime
.borrow_mut()
.dispatch_pointer_scroll(*point, crate::Dp::new(*delta_y as f32));
dispatch(report, &mut reports);
}
crate::NativeViewSmokeInput::WindowCloseRequest => {
let report = self.runtime.borrow_mut().dispatch_window_close_requested();
dispatch(report, &mut reports);
}
}
}
schedule_linux_gtk_runtime_tick(
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.clone(),
&self.runtime_timer,
);
reports
}
pub(crate) fn capture_png(
&self,
path: &Path,
) -> Result<crate::NativeViewCaptureEvidence, String> {
if !self.area.is_mapped() {
return Err("the GTK proof window is not mapped".into());
}
let logical_width = self.area.width().max(0) as u32;
let logical_height = self.area.height().max(0) as u32;
if logical_width == 0 || logical_height == 0 {
return Err("the GTK DrawingArea has an empty allocation".to_string());
}
let native = self
.area
.native()
.ok_or_else(|| "the GTK DrawingArea is not attached to a native surface".to_string())?;
let renderer = native
.renderer()
.ok_or_else(|| "GTK did not provide a GSK renderer for the DrawingArea".to_string())?;
let scale_factor = self.area.scale_factor().max(1) as f32;
let snapshot = gtk::Snapshot::new();
snapshot.scale(scale_factor, scale_factor);
self.paintable.snapshot(
&snapshot,
f64::from(logical_width),
f64::from(logical_height),
);
let node = snapshot
.to_node()
.ok_or_else(|| "GTK produced an empty render node for the DrawingArea".to_string())?;
let viewport = gtk::graphene::Rect::new(
0.0,
0.0,
logical_width as f32 * scale_factor,
logical_height as f32 * scale_factor,
);
let texture = renderer.render_texture(&node, Some(&viewport));
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent)
.map_err(|error| format!("could not create GTK capture directory: {error}"))?;
}
texture
.save_to_png(path)
.map_err(|error| format!("could not write GTK PNG capture: {error}"))?;
Ok(crate::NativeViewCaptureEvidence {
platform: "linux",
backend: "gtk_widget_paintable_gsk_texture",
display_server: None,
logical_width,
logical_height,
pixel_width: texture.width().max(1) as u32,
pixel_height: texture.height().max(1) as u32,
scale_factor: f64::from(scale_factor),
typography_scale: self.plan.borrow().typography_scale(),
typography: linux_gtk_native_typography_profile(
self.plan.borrow().typography_scale(),
Some(&self.area.pango_context()),
),
})
}
pub(crate) fn set_window_suspended(&self, suspended: bool) {
if suspended {
if !self.runtime.borrow_mut().suspend_view_when_hidden() {
return;
}
if let Some(source) = self.runtime_timer.borrow_mut().take() {
source.remove();
}
*self.plan.borrow_mut() = NativeDrawPlan::default();
self.ime.reset();
self.ime.focus_out();
self.area.queue_draw();
return;
}
let Some(plan) = self.runtime.borrow_mut().resume_view_when_visible() else {
return;
};
*self.plan.borrow_mut() = plan;
self.area.queue_draw();
schedule_linux_gtk_runtime_tick(
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.clone(),
&self.runtime_timer,
);
}
pub(crate) fn dispatch_app_command(&self, command: crate::Command) {
let report = self.runtime.borrow_mut().dispatch_app_command(command);
apply_linux_gtk_input_report(
report,
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.as_ref(),
);
schedule_linux_gtk_runtime_tick(
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.clone(),
&self.runtime_timer,
);
}
pub(crate) fn dispatch_window_close_requested(&self) -> bool {
let report = self.runtime.borrow_mut().dispatch_window_close_requested();
let allow = !report.handled || report.quit_requested;
apply_linux_gtk_input_report(
report,
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.as_ref(),
);
schedule_linux_gtk_runtime_tick(
&self.area,
&self.plan,
&self.runtime,
&self.ime,
self.application.clone(),
&self.runtime_timer,
);
allow
}
}
fn schedule_linux_gtk_runtime_tick(
area: >k::DrawingArea,
plan: &Rc<RefCell<NativeDrawPlan>>,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
application: Option<gtk::Application>,
timer: &Rc<RefCell<Option<gtk::glib::SourceId>>>,
) {
if let Some(source) = timer.borrow_mut().take() {
source.remove();
}
let Some(delay_ms) = runtime.borrow().transient_poll_interval_ms() else {
return;
};
let area = area.clone();
let plan = Rc::clone(plan);
let runtime = Rc::clone(runtime);
let ime = ime.clone();
let timer_for_callback = Rc::clone(timer);
let source = gtk::glib::timeout_add_local_once(
std::time::Duration::from_millis(delay_ms.max(1)),
move || {
timer_for_callback.borrow_mut().take();
let report = runtime.borrow_mut().refresh_transient_view();
apply_linux_gtk_input_report(
report,
&area,
&plan,
&runtime,
&ime,
application.as_ref(),
);
schedule_linux_gtk_runtime_tick(
&area,
&plan,
&runtime,
&ime,
application,
&timer_for_callback,
);
},
);
*timer.borrow_mut() = Some(source);
}
#[cfg(feature = "window-chrome")]
fn begin_linux_gtk_window_gesture(
gesture: >k::GestureClick,
area: >k::DrawingArea,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
x: f64,
y: f64,
) -> bool {
let Some(window) = area
.root()
.and_then(|root| root.downcast::<gtk::Window>().ok())
else {
return false;
};
let Some(top) = window
.surface()
.and_then(|surface| surface.downcast::<gtk::gdk::Toplevel>().ok())
else {
return false;
};
let Some(event) = gesture.current_event() else {
return false;
};
let Some(device) = event.device() else {
return false;
};
if !window.is_decorated() && window.is_resizable() && !window.is_maximized() {
let left = x < 6.0;
let right = x >= f64::from(area.width()) - 6.0;
let upper = y < 6.0;
let lower = y >= f64::from(area.height()) - 6.0;
use gtk::gdk::SurfaceEdge as E;
let edge = match (left, right, upper, lower) {
(true, _, true, _) => Some(E::NorthWest),
(_, true, true, _) => Some(E::NorthEast),
(true, _, _, true) => Some(E::SouthWest),
(_, true, _, true) => Some(E::SouthEast),
(true, _, _, _) => Some(E::West),
(_, true, _, _) => Some(E::East),
(_, _, true, _) => Some(E::North),
(_, _, _, true) => Some(E::South),
_ => None,
};
if let Some(edge) = edge {
top.begin_resize(edge, Some(&device), 1, x, y, event.time());
return true;
}
}
let point = crate::Point {
x: gtk_coordinate(x),
y: gtk_coordinate(y),
};
if runtime.borrow().window_drag_region_at(point) {
top.begin_move(&device, 1, x, y, event.time());
return true;
}
false
}
fn apply_linux_gtk_input_report(
report: crate::native::NativeViewInputDispatchReport,
area: >k::DrawingArea,
plan: &Rc<RefCell<NativeDrawPlan>>,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
application: Option<>k::Application>,
) -> crate::native::NativeViewInputDispatchReport {
apply_linux_gtk_input_report_with_redraw_request(
report,
area,
plan,
runtime,
ime,
application,
true,
)
}
fn apply_linux_gtk_input_report_during_draw(
report: crate::native::NativeViewInputDispatchReport,
area: >k::DrawingArea,
plan: &Rc<RefCell<NativeDrawPlan>>,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
application: Option<>k::Application>,
) -> crate::native::NativeViewInputDispatchReport {
apply_linux_gtk_input_report_with_redraw_request(
report,
area,
plan,
runtime,
ime,
application,
false,
)
}
fn apply_linux_gtk_input_report_with_redraw_request(
mut report: crate::native::NativeViewInputDispatchReport,
area: >k::DrawingArea,
plan: &Rc<RefCell<NativeDrawPlan>>,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
application: Option<>k::Application>,
request_redraw: bool,
) -> crate::native::NativeViewInputDispatchReport {
let dispatch_turn = runtime.borrow_mut().begin_deferred_dispatch();
if dispatch_turn.is_some() {
let (executor, commands) = runtime.borrow_mut().take_pending_app_command_dispatch();
let effect_executed = crate::native::dispatch_deferred_native_view_app_commands(
&mut report,
executor,
commands,
dispatch_turn.as_ref(),
);
if effect_executed {
runtime
.borrow_mut()
.refresh_live_view_after_app_effect(&mut report);
}
let (ui_executor, ui_commands) = runtime.borrow_mut().take_pending_ui_command_dispatch();
if let Some(executor) = ui_executor {
for command in ui_commands {
if dispatch_turn.as_ref().is_none_or(|turn| !turn.is_alive()) {
break;
}
if let Err(error) = executor.dispatch(command) {
report.errors.push(error.to_string());
}
}
}
let mut budget = crate::dispatch_queue::DispatchTurnBudget::new();
while budget.take() {
let request = runtime.borrow_mut().pop_pending_app_effect();
let Some(request) = request else {
break;
};
if !request.try_begin() {
continue;
}
let outcome = crate::app_effect::execute_native_app_effect(request.effect());
let effect_report = runtime
.borrow_mut()
.dispatch_app_effect_outcome(request, outcome);
report.handled |= effect_report.handled;
report.message_count += effect_report.message_count;
report.app_effect_outcome_count += effect_report.app_effect_outcome_count;
report.quit_requested |= effect_report.quit_requested;
#[cfg(feature = "window-chrome")]
report.window_commands.extend(effect_report.window_commands);
report.errors.extend(effect_report.errors);
if let Some(plan) = effect_report.redraw_plan {
report.redraw_plan = Some(plan);
}
}
}
drop(dispatch_turn);
#[cfg(feature = "window-chrome")]
if let Some(window) = area
.root()
.and_then(|root| root.downcast::<gtk::Window>().ok())
{
for command in &report.window_commands {
match command {
crate::ZsWindowCommand::Minimize => window.minimize(),
crate::ZsWindowCommand::ToggleMaximize => {
if window.is_maximized() {
window.unmaximize()
} else {
window.maximize()
}
}
crate::ZsWindowCommand::Close => window.close(),
crate::ZsWindowCommand::Hide => {
window.set_visible(false);
report.redraw_plan = None;
if runtime.borrow_mut().suspend_view_when_hidden() {
*plan.borrow_mut() = NativeDrawPlan::default();
}
}
crate::ZsWindowCommand::Show => {
window.unminimize();
report.redraw_plan = runtime.borrow_mut().resume_view_when_visible();
window.present();
}
}
}
if !report.window_commands.is_empty() && !runtime.borrow().is_view_suspended() {
report.redraw_plan = runtime.borrow().current_composed_draw_plan();
}
}
if let Some(updated) = report.redraw_plan.take() {
*plan.borrow_mut() = updated;
if request_redraw {
area.queue_draw();
}
}
if report.quit_requested {
if let Some(application) = application {
application.quit();
}
}
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
sync_linux_gtk_text_accessibility(area, runtime);
sync_linux_gtk_ime(area, runtime, ime);
report
}
#[cfg(all(feature = "accessibility", feature = "text-input-core"))]
fn sync_linux_gtk_text_accessibility(
area: >k::DrawingArea,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
) {
let snapshot = runtime.borrow().focused_text_accessibility_snapshot();
if let Some(snapshot) = snapshot {
area.update_property(&[
gtk::accessible::Property::ValueText(snapshot.exposed_text()),
gtk::accessible::Property::MultiLine(snapshot.kind().is_multiline()),
gtk::accessible::Property::ReadOnly(false),
]);
area.update_state(&[gtk::accessible::State::Hidden(false)]);
} else {
area.reset_property(gtk::AccessibleProperty::ValueText);
area.reset_property(gtk::AccessibleProperty::MultiLine);
area.reset_property(gtk::AccessibleProperty::ReadOnly);
area.reset_state(gtk::AccessibleState::Hidden);
}
}
fn sync_linux_gtk_ime(
area: >k::DrawingArea,
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
) {
let (accepts_committed_text, caret_rect, surrounding) = {
let runtime = runtime.borrow();
(
runtime.accepts_committed_text_input(),
runtime.text_input_caret_rect(),
runtime.focused_text_input_snapshot(),
)
};
if area.has_focus() && accepts_committed_text {
if let Some(rect) = caret_rect {
ime.set_cursor_location(>k::gdk::Rectangle::new(
rect.x,
rect.y,
rect.width.max(1),
rect.height.max(1),
));
}
if let Some((value, selection)) = surrounding {
let cursor = crate::native_text_edit::char_to_byte_index(&value, selection.caret)
.min(i32::MAX as usize) as i32;
let anchor = crate::native_text_edit::char_to_byte_index(&value, selection.anchor)
.min(i32::MAX as usize) as i32;
ime.set_surrounding_with_selection(&value, cursor, anchor);
}
ime.focus_in();
} else {
ime.focus_out();
}
}
fn reset_linux_gtk_ime_if_no_text_target(
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
ime: >k::IMMulticontext,
) {
let accepts_committed_text = runtime.borrow().accepts_committed_text_input();
if !accepts_committed_text {
ime.reset();
}
}
fn dispatch_linux_gtk_ime_preedit(
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
text: &str,
cursor: Option<usize>,
) -> crate::native::NativeViewInputDispatchReport {
runtime
.borrow_mut()
.dispatch_ime_preedit(text, cursor.map(|cursor| (cursor, cursor)))
}
fn dispatch_linux_gtk_ime_commit(
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
text: &str,
) -> crate::native::NativeViewInputDispatchReport {
runtime.borrow_mut().dispatch_ime_commit(text)
}
fn dispatch_linux_gtk_ime_cancel(
runtime: &Rc<RefCell<crate::native::NativeViewInputRuntime>>,
) -> crate::native::NativeViewInputDispatchReport {
runtime.borrow_mut().cancel_ime_preedit()
}
fn gtk_coordinate(value: f64) -> i32 {
value
.round()
.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
}
fn linux_gtk_pointer_modifiers(modifiers: gtk::gdk::ModifierType) -> crate::ZsPointerModifiers {
crate::ZsPointerModifiers::new(
modifiers.contains(gtk::gdk::ModifierType::SHIFT_MASK),
modifiers.contains(gtk::gdk::ModifierType::CONTROL_MASK),
modifiers.contains(gtk::gdk::ModifierType::ALT_MASK),
modifiers
.intersects(gtk::gdk::ModifierType::SUPER_MASK | gtk::gdk::ModifierType::META_MASK),
)
}
fn linux_gtk_pointer_button(button: u32) -> crate::ZsPointerButton {
match button {
gtk::gdk::BUTTON_PRIMARY => crate::ZsPointerButton::Primary,
gtk::gdk::BUTTON_SECONDARY => crate::ZsPointerButton::Secondary,
gtk::gdk::BUTTON_MIDDLE => crate::ZsPointerButton::Middle,
button => crate::ZsPointerButton::Auxiliary(button.min(u32::from(u16::MAX)) as u16),
}
}
fn linux_gtk_system_appearance() -> (bool, bool) {
let Some(settings) = gtk::Settings::default() else {
return (false, false);
};
let theme_name = settings
.gtk_theme_name()
.map(|name| name.to_ascii_lowercase())
.unwrap_or_default();
(
settings.is_gtk_application_prefer_dark_theme()
|| theme_name.contains("dark")
|| theme_name.contains("inverse"),
linux_gtk_theme_name_is_high_contrast(&theme_name),
)
}
fn linux_gtk_theme_name_is_high_contrast(theme_name: &str) -> bool {
let compact = theme_name
.chars()
.filter(|character| character.is_ascii_alphanumeric())
.collect::<String>();
compact.contains("highcontrast")
}
#[allow(deprecated)]
fn linux_gtk_semantic_high_contrast_palette(area: >k::DrawingArea) -> Option<NativeDrawPalette> {
let context = area.style_context();
let primary_text = linux_gtk_color(context.color());
let surface = linux_gtk_lookup_color(
&context,
&["window_bg_color", "theme_bg_color", "view_bg_color"],
)?;
let accent = linux_gtk_lookup_color(
&context,
&[
"accent_bg_color",
"theme_selected_bg_color",
"theme_selected_bg_color_breeze",
],
)?;
let accent_text =
linux_gtk_lookup_color(&context, &["accent_fg_color", "theme_selected_fg_color"])?;
let control = linux_gtk_lookup_color(
&context,
&["view_bg_color", "theme_base_color", "window_bg_color"],
)
.unwrap_or(surface);
Some(NativeDrawPalette {
primary_text,
secondary_text: primary_text,
disabled_text: primary_text,
accent,
accent_text,
surface,
surface_raised: control,
control,
border: primary_text,
strong_stroke: primary_text,
success: primary_text,
warning: primary_text,
danger: primary_text,
high_contrast: true,
})
}
#[allow(deprecated)]
fn linux_gtk_semantic_palette(area: >k::DrawingArea) -> Option<NativeDrawPalette> {
let context = area.style_context();
let primary_text = linux_gtk_lookup_color(
&context,
&["window_fg_color", "theme_fg_color", "view_fg_color"],
)
.unwrap_or_else(|| linux_gtk_color(context.color()));
let surface = linux_gtk_lookup_color(
&context,
&["window_bg_color", "theme_bg_color", "view_bg_color"],
)?;
let surface_raised = linux_gtk_lookup_color(
&context,
&["card_bg_color", "view_bg_color", "theme_base_color"],
)
.unwrap_or(surface);
let control = linux_gtk_lookup_color(
&context,
&["view_bg_color", "card_bg_color", "theme_base_color"],
)
.unwrap_or(surface_raised);
Some(NativeDrawPalette {
primary_text,
secondary_text: linux_gtk_lookup_color(
&context,
&["dim_label_fg_color", "theme_unfocused_fg_color"],
)
.unwrap_or(primary_text),
disabled_text: linux_gtk_lookup_color(
&context,
&["insensitive_fg_color", "theme_unfocused_fg_color"],
)
.unwrap_or(primary_text),
accent: linux_gtk_lookup_color(&context, &["accent_bg_color", "theme_selected_bg_color"])?,
accent_text: linux_gtk_lookup_color(
&context,
&["accent_fg_color", "theme_selected_fg_color"],
)
.unwrap_or(surface_raised),
surface,
surface_raised,
control,
border: linux_gtk_lookup_color(
&context,
&["headerbar_border_color", "borders", "shade_color"],
)
.unwrap_or(primary_text),
strong_stroke: crate::native_draw_support::strong_stroke_color(primary_text, surface),
success: linux_gtk_lookup_color(&context, &["success_bg_color", "success_color"])
.unwrap_or(Color::rgb(46, 160, 67)),
warning: linux_gtk_lookup_color(&context, &["warning_bg_color", "warning_color"])
.unwrap_or(Color::rgb(229, 165, 10)),
danger: linux_gtk_lookup_color(
&context,
&["destructive_bg_color", "error_bg_color", "error_color"],
)
.unwrap_or(Color::rgb(192, 28, 40)),
high_contrast: false,
})
}
#[allow(deprecated)]
fn linux_gtk_lookup_color(context: >k::StyleContext, names: &[&str]) -> Option<Color> {
names
.iter()
.find_map(|name| context.lookup_color(name))
.map(linux_gtk_color)
}
fn linux_gtk_color(color: gtk::gdk::RGBA) -> Color {
Color::rgba(
linux_gtk_color_channel(color.red()),
linux_gtk_color_channel(color.green()),
linux_gtk_color_channel(color.blue()),
linux_gtk_color_channel(color.alpha()),
)
}
fn linux_gtk_color_channel(value: f32) -> u8 {
(value * 255.0).round().clamp(0.0, 255.0) as u8
}
pub struct LinuxGtkTextLayout {
context: gtk::pango::Context,
}
impl LinuxGtkTextLayout {
pub fn new(context: gtk::pango::Context) -> Self {
Self { context }
}
fn pango_layout(
&self,
text: &str,
style: &TextStyle,
bounds: Option<Rect>,
) -> gtk::pango::Layout {
let layout = gtk::pango::Layout::new(&self.context);
configure_pango_layout(&layout, text, style, bounds);
layout
}
}
impl TextLayout for LinuxGtkTextLayout {
fn measure(&self, text: &str, style: &TextStyle, max_width: i32) -> Size {
if text.is_empty() {
return Size {
width: 0,
height: 0,
};
}
let bounds = (max_width > 0).then(|| Rect {
x: 0,
y: 0,
width: max_width,
height: i32::MAX / 4,
});
let layout = self.pango_layout(text, style, bounds);
let (width, height) = layout.pixel_size();
Size {
width: width.max(0),
height: height.max(0),
}
}
fn layout_runs(&self, text: &str, _style: &TextStyle, bounds: Rect) -> Vec<TextRun> {
if text.is_empty() {
Vec::new()
} else {
vec![TextRun {
text: text.to_string(),
bounds,
}]
}
}
}
#[cfg(feature = "text-input-core")]
fn linux_gtk_text_shaping_backend(
context: gtk::pango::Context,
) -> crate::native_input_visuals::NativeTextShapingBackend {
crate::native_input_visuals::NativeTextShapingBackend::platform(LinuxGtkPangoTextShaper {
context,
scale: std::sync::Mutex::new(1.0),
})
}
#[cfg(feature = "text-input-core")]
struct LinuxGtkPangoTextShaper {
scale: std::sync::Mutex<f32>,
context: gtk::pango::Context,
}
#[cfg(feature = "text-input-core")]
impl crate::native_input_visuals::NativeTextShaper for LinuxGtkPangoTextShaper {
fn set_context(&self, _dpi: crate::Dpi, scale: f32) {
*self
.scale
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = scale;
}
fn debug_name(&self) -> &'static str {
"Gtk(PangoContext)"
}
fn typography_scale(&self) -> f32 {
*self
.scale
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn measure(
&self,
text: &str,
semantic: crate::SemanticTextStyle,
max_width: i32,
_dpi: crate::Dpi,
typography_scale: f32,
) -> Option<Size> {
let resolver = NativeDrawTextStyleResolver::from_profile(
linux_gtk_native_typography_profile(typography_scale, Some(&self.context)),
NativeDrawPalette::for_mode(crate::ZsuiThemeMode::Light, false),
);
let style = resolver.resolve_text_style(semantic);
Some(LinuxGtkTextLayout::new(self.context.clone()).measure(text, &style, max_width))
}
fn shape_line(&self, text: &str) -> Option<crate::native_input_visuals::NativeShapedTextLine> {
shape_linux_gtk_text_line_at_scale(&self.context, text, self.typography_scale())
}
}
#[cfg(feature = "text-input-core")]
pub(crate) fn shape_linux_gtk_text_line(
context: >k::pango::Context,
text: &str,
) -> Option<crate::native_input_visuals::NativeShapedTextLine> {
shape_linux_gtk_text_line_at_scale(context, text, linux_gtk_ui_font_scale())
}
#[cfg(feature = "text-input-core")]
fn shape_linux_gtk_text_line_at_scale(
context: >k::pango::Context,
text: &str,
typography_scale: f32,
) -> Option<crate::native_input_visuals::NativeShapedTextLine> {
use crate::native_input_visuals::{
NativeShapedTextCaret, NativeShapedTextCluster, NativeShapedTextLine,
};
if text.is_empty() {
return None;
}
let body = crate::TextRole::Body.metrics_for(crate::ZsTypographyPlatformStyle::Gtk);
let mut style = TextStyle::line(
linux_gtk_ui_font_family(),
body.size * typography_scale,
Color::rgb(0, 0, 0),
);
style.line_height = body.line_height * typography_scale;
style.semantic_role = Some(crate::TextRole::Body);
let layout = gtk::pango::Layout::new(context);
configure_pango_layout(&layout, text, &style, None);
let boundaries = crate::native_text_edit::grapheme_boundaries(text);
let byte_offsets = boundaries
.iter()
.map(|index| i32::try_from(crate::native_text_edit::char_to_byte_index(text, *index)).ok())
.collect::<Option<Vec<_>>>()?;
let carets = boundaries
.iter()
.copied()
.zip(byte_offsets.iter().copied())
.map(|(index, byte)| {
let (strong, weak) = layout.cursor_pos(byte);
NativeShapedTextCaret {
index,
primary_x: pango_coordinate(strong.x()),
secondary_x: pango_coordinate(weak.x()),
}
})
.collect::<Vec<_>>();
let clusters = boundaries
.windows(2)
.zip(byte_offsets.iter().copied())
.map(|(scalar, byte)| {
let position = layout.index_to_pos(byte);
NativeShapedTextCluster {
start: scalar[0],
end: scalar[1],
start_x: pango_coordinate(position.x()),
end_x: pango_coordinate(position.x().saturating_add(position.width())),
}
})
.collect::<Vec<_>>();
let (width, _) = layout.pixel_size();
NativeShapedTextLine::new(width, clusters, carets)
}
#[cfg(feature = "text-input-core")]
fn pango_coordinate(value: i32) -> i32 {
let scale = gtk::pango::SCALE.max(1);
if value >= 0 {
value.saturating_add(scale / 2) / scale
} else {
value.saturating_sub(scale / 2) / scale
}
}
struct LinuxGtkDrawSink<'a> {
area: &'a gtk::DrawingArea,
context: &'a gtk::cairo::Context,
palette: NativeDrawPalette,
style_resolver: NativeDrawTextStyleResolver,
text_layout: LinuxGtkTextLayout,
clip_depth: usize,
}
impl<'a> LinuxGtkDrawSink<'a> {
fn new(
area: &'a gtk::DrawingArea,
context: &'a gtk::cairo::Context,
palette: NativeDrawPalette,
typography_scale: f32,
) -> Self {
Self {
area,
context,
palette,
style_resolver: NativeDrawTextStyleResolver::from_profile(
linux_gtk_native_typography_profile(typography_scale, Some(&area.pango_context())),
palette,
),
text_layout: LinuxGtkTextLayout::new(area.pango_context()),
clip_depth: 0,
}
}
fn set_source(&self, color: Color) {
self.context.set_source_rgba(
f64::from(color.r) / 255.0,
f64::from(color.g) / 255.0,
f64::from(color.b) / 255.0,
f64::from(color.a) / 255.0,
);
}
fn add_rect(&self, rect: Rect) {
self.context.rectangle(
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width.max(0)),
f64::from(rect.height.max(0)),
);
}
fn add_round_rect(&self, rect: Rect, radius: i32) {
let x = f64::from(rect.x);
let y = f64::from(rect.y);
let width = f64::from(rect.width.max(0));
let height = f64::from(rect.height.max(0));
let radius = f64::from(radius.max(0)).min(width / 2.0).min(height / 2.0);
if radius <= 0.0 {
self.context.rectangle(x, y, width, height);
return;
}
let right = x + width;
let bottom = y + height;
self.context.new_sub_path();
self.context.arc(
right - radius,
y + radius,
radius,
-std::f64::consts::FRAC_PI_2,
0.0,
);
self.context.arc(
right - radius,
bottom - radius,
radius,
0.0,
std::f64::consts::FRAC_PI_2,
);
self.context.arc(
x + radius,
bottom - radius,
radius,
std::f64::consts::FRAC_PI_2,
std::f64::consts::PI,
);
self.context.arc(
x + radius,
y + radius,
radius,
std::f64::consts::PI,
std::f64::consts::PI * 1.5,
);
self.context.close_path();
}
fn draw_text(&self, command: &NativeDrawTextCommand) {
let mut style = self.style_resolver.resolve_text_style(command.style);
if let Some(color) = command.color_override(self.palette.high_contrast) {
style.color = color;
}
command.apply_size_override(&mut style);
let layout = self
.text_layout
.pango_layout(&command.text, &style, Some(command.bounds));
let (text_width, text_height) = layout.pixel_size();
let x = if style.wrap == TextWrap::NoWrap && !style.ellipsis {
match style.horizontal_align {
HorizontalAlign::Start => command.bounds.x,
HorizontalAlign::Center => {
command.bounds.x + (command.bounds.width - text_width).max(0) / 2
}
HorizontalAlign::End => {
command.bounds.x + (command.bounds.width - text_width).max(0)
}
}
} else {
command.bounds.x
};
let y = match style.vertical_align {
VerticalAlign::Start => command.bounds.y,
VerticalAlign::Center => {
command.bounds.y + (command.bounds.height - text_height).max(0) / 2
}
VerticalAlign::End => command.bounds.y + (command.bounds.height - text_height).max(0),
};
if self.context.save().is_err() {
return;
}
self.add_rect(command.bounds);
self.context.clip();
self.set_source(style.color);
self.context.move_to(f64::from(x), f64::from(y));
unsafe {
pango_cairo_show_layout(self.context.to_raw_none(), layout.to_glib_none().0);
}
let _ = self.context.restore();
}
fn draw_icon(&self, command: &NativeDrawIconCommand) {
if command.bounds.width <= 0 || command.bounds.height <= 0 {
return;
}
let size = command.bounds.width.min(command.bounds.height).max(1);
let raster_size = size
.saturating_mul(self.area.scale_factor().max(1))
.min(512);
let theme = gtk::IconTheme::for_display(&self.area.display());
let theme_aware = command.color_mode == NativeIconColorMode::ThemeAware;
let flags = if theme_aware {
gtk::IconLookupFlags::FORCE_SYMBOLIC
} else {
gtk::IconLookupFlags::empty()
};
let pixbuf = if theme.has_icon(command.icon.gtk_symbolic_name()) {
let paintable = theme.lookup_icon(
command.icon.gtk_symbolic_name(),
&[],
raster_size,
1,
gtk::TextDirection::None,
flags,
);
paintable
.file()
.and_then(|file| file.path())
.and_then(|path| {
gtk::gdk_pixbuf::Pixbuf::from_file_at_scale(
path,
raster_size,
raster_size,
true,
)
.ok()
})
} else {
None
};
let pixbuf = pixbuf.or_else(|| {
let loader = gtk::gdk_pixbuf::PixbufLoader::with_type("svg").ok()?;
loader.set_size(raster_size, raster_size);
loader
.write(crate::bundled_fluent_icon_svg(command.icon))
.ok()?;
loader.close().ok()?;
loader.pixbuf()
});
let Some(pixbuf) = pixbuf else {
crate::symbolic_icons::draw_symbolic_icon(
&self.context,
command,
command
.color_override(self.palette.high_contrast)
.unwrap_or_else(|| self.palette.resolve(command.color)),
);
return;
};
let bounds = command.fitted_bounds(crate::Size {
width: pixbuf.width(),
height: pixbuf.height(),
});
if self.context.save().is_err() {
return;
}
self.context.rectangle(
f64::from(command.bounds.x),
f64::from(command.bounds.y),
f64::from(command.bounds.width),
f64::from(command.bounds.height),
);
self.context.clip();
self.context
.translate(f64::from(bounds.x), f64::from(bounds.y));
self.context.scale(
f64::from(bounds.width) / f64::from(pixbuf.width().max(1)),
f64::from(bounds.height) / f64::from(pixbuf.height().max(1)),
);
self.context.set_source_pixbuf(&pixbuf, 0.0, 0.0);
if theme_aware {
let mask = self.context.source();
let color = command
.color_override(self.palette.high_contrast)
.unwrap_or_else(|| self.palette.resolve(command.color));
self.context.set_source_rgba(
f64::from(color.r) / 255.0,
f64::from(color.g) / 255.0,
f64::from(color.b) / 255.0,
f64::from(color.a) / 255.0,
);
let _ = self.context.mask(&mask);
} else {
let _ = self.context.paint();
}
let _ = self.context.restore();
}
fn draw_image(&self, command: &NativeDrawImageCommand) {
let Ok(width) = i32::try_from(command.frame.width()) else {
return;
};
let Ok(height) = i32::try_from(command.frame.height()) else {
return;
};
if width <= 0
|| height <= 0
|| command.source.width <= 0
|| command.source.height <= 0
|| command.bounds.width <= 0
|| command.bounds.height <= 0
{
return;
}
let Ok(stride) = gtk::cairo::Format::ARgb32.stride_for_width(command.frame.width()) else {
return;
};
if usize::try_from(stride).ok().and_then(|stride| {
usize::try_from(height)
.ok()
.and_then(|height| stride.checked_mul(height))
}) != Some(command.frame.decoded_bytes())
{
return;
}
let Ok(surface) = gtk::cairo::ImageSurface::create_for_data(
command.frame.premultiplied_bgra8().to_vec(),
gtk::cairo::Format::ARgb32,
width,
height,
stride,
) else {
return;
};
if self.context.save().is_err() {
return;
}
self.add_rect(command.bounds);
self.context.clip();
self.context
.translate(f64::from(command.bounds.x), f64::from(command.bounds.y));
self.context.scale(
f64::from(command.bounds.width) / f64::from(command.source.width),
f64::from(command.bounds.height) / f64::from(command.source.height),
);
if self
.context
.set_source_surface(
&surface,
-f64::from(command.source.x),
-f64::from(command.source.y),
)
.is_ok()
{
self.context
.source()
.set_filter(match command.interpolation {
NativeImageInterpolation::Nearest => gtk::cairo::Filter::Nearest,
NativeImageInterpolation::Smooth => gtk::cairo::Filter::Good,
});
let _ = self.context.paint();
}
let _ = self.context.restore();
}
fn push_clip(&mut self, rect: Rect) {
if self.context.save().is_ok() {
self.add_rect(rect);
self.context.clip();
self.clip_depth += 1;
}
}
fn pop_clip(&mut self) {
if self.clip_depth > 0 {
let _ = self.context.restore();
self.clip_depth -= 1;
}
}
}
impl Drop for LinuxGtkDrawSink<'_> {
fn drop(&mut self) {
while self.clip_depth > 0 {
self.pop_clip();
}
}
}
impl NativeDrawCommandSink for LinuxGtkDrawSink<'_> {
fn draw_command(&mut self, command: &NativeDrawCommand) {
match command {
NativeDrawCommand::FillRect { rect, fill } => {
self.set_source(self.palette.resolve_source_fill(*fill));
self.add_rect(*rect);
let _ = self.context.fill();
}
NativeDrawCommand::StrokeRect {
rect,
stroke,
width,
} => {
self.set_source(self.palette.resolve_source_fill(*stroke));
self.context.set_line_width(f64::from((*width).max(1)));
self.add_rect(*rect);
let _ = self.context.stroke();
}
NativeDrawCommand::StrokeArc {
rect,
stroke,
width,
start_degrees,
sweep_degrees,
} => {
self.set_source(self.palette.resolve_source_fill(*stroke));
self.context.set_line_width(f64::from((*width).max(1)));
let start = f64::from(*start_degrees).to_radians();
let end = f64::from(start_degrees.saturating_add(*sweep_degrees)).to_radians();
self.context.arc(
f64::from(rect.x) + f64::from(rect.width) / 2.0,
f64::from(rect.y) + f64::from(rect.height) / 2.0,
f64::from(rect.width.min(rect.height).max(0)) / 2.0,
start,
end,
);
let _ = self.context.stroke();
}
NativeDrawCommand::FillTriangle { points, fill } => {
self.set_source(self.palette.resolve_source_fill(*fill));
self.context
.move_to(f64::from(points[0].x), f64::from(points[0].y));
for point in &points[1..] {
self.context.line_to(f64::from(point.x), f64::from(point.y));
}
self.context.close_path();
let _ = self.context.fill();
}
NativeDrawCommand::RoundRect {
rect,
fill,
stroke,
radius,
} => {
self.add_round_rect(*rect, *radius);
self.set_source(self.palette.resolve_source_fill(*fill));
if stroke.is_some() {
let _ = self.context.fill_preserve();
} else {
let _ = self.context.fill();
}
if let Some(stroke) = stroke {
self.set_source(self.palette.resolve_source_fill(*stroke));
self.context.set_line_width(1.0);
let _ = self.context.stroke();
}
}
NativeDrawCommand::RoundFill { rect, fill, radius } => {
self.add_round_rect(*rect, *radius);
self.set_source(self.palette.resolve_source_fill(*fill));
let _ = self.context.fill();
}
NativeDrawCommand::Text(command) => self.draw_text(command),
#[cfg(feature = "password-box")]
NativeDrawCommand::SecureText(command) => {
let rendered = command.rendered_text();
self.draw_text(&NativeDrawTextCommand::new(
rendered.as_str(),
command.bounds,
command.style,
));
}
NativeDrawCommand::Icon(command) => self.draw_icon(command),
NativeDrawCommand::Image(command) => self.draw_image(command),
NativeDrawCommand::PushClip { rect } => self.push_clip(*rect),
NativeDrawCommand::PopClip => self.pop_clip(),
}
}
}
fn linux_gtk_ui_font_family() -> String {
linux_gtk_configured_font_description()
.and_then(|(_, description)| description.family().map(|family| family.to_string()))
.filter(|family| !family.trim().is_empty())
.unwrap_or_else(|| "Adwaita Sans".to_string())
}
pub(crate) fn linux_gtk_ui_font_scale() -> f32 {
let Some((_, description)) = linux_gtk_configured_font_description() else {
return 1.0;
};
let configured_size = description.size();
if configured_size <= 0 {
return 1.0;
}
let logical_pixels = if description.is_size_absolute() {
configured_size as f32 / gtk::pango::SCALE as f32
} else {
configured_size as f32 / gtk::pango::SCALE as f32 * (96.0 / 72.0)
};
(logical_pixels / 14.0).clamp(0.75, 3.0)
}
fn linux_gtk_configured_font_description() -> Option<(String, gtk::pango::FontDescription)> {
let name = gtk::Settings::default()?.gtk_font_name()?.to_string();
let description = gtk::pango::FontDescription::from_string(&name);
Some((name, description))
}
fn linux_gtk_native_typography_profile(
typography_scale: f32,
context: Option<>k::pango::Context>,
) -> crate::NativeTypographyProfile {
let configured = linux_gtk_configured_font_description();
let font_family = configured
.as_ref()
.and_then(|(_, description)| description.family())
.map(|family| family.to_string())
.filter(|family| !family.trim().is_empty())
.unwrap_or_else(|| "Adwaita Sans".to_string());
let mut profile = crate::NativeTypographyProfile::new(
crate::ZsTypographyPlatformStyle::Gtk,
"gtk_settings_pango_context",
font_family.clone(),
"Monospace",
font_family,
typography_scale,
"pango_cairo",
);
if let Some((name, _)) = configured {
profile = profile.with_configured_ui_font(name);
}
if let Some(context) = context {
let mut font = gtk::pango::FontDescription::new();
font.set_family(&profile.ui_font_family);
font.set_absolute_size(f64::from(profile.body_metrics.size) * f64::from(gtk::pango::SCALE));
let metrics = context.metrics(Some(&font), None);
let ascent = metrics.ascent() as f32 / gtk::pango::SCALE as f32;
let descent = metrics.descent() as f32 / gtk::pango::SCALE as f32;
let leading = (profile.body_metrics.line_height - ascent - descent).max(0.0);
profile = profile.with_body_vertical_metrics(ascent, descent, leading);
}
profile
}
fn configure_pango_layout(
layout: >k::pango::Layout,
text: &str,
style: &TextStyle,
bounds: Option<Rect>,
) {
layout.set_text(text);
let mut font = gtk::pango::FontDescription::new();
font.set_family(&style.font_family);
font.set_absolute_size(f64::from(style.size) * f64::from(gtk::pango::SCALE));
font.set_weight(match style.weight {
TextWeight::Automatic => gtk::pango::Weight::Normal,
TextWeight::Regular => gtk::pango::Weight::Normal,
TextWeight::Medium => gtk::pango::Weight::Medium,
TextWeight::Semibold => gtk::pango::Weight::Semibold,
TextWeight::Bold => gtk::pango::Weight::Bold,
});
layout.set_font_description(Some(&font));
if style.line_height > 0.0 {
let metrics = layout.context().metrics(Some(&font), None);
let natural_line_height = metrics.ascent().saturating_add(metrics.descent());
let target_line_height = (f64::from(style.line_height.max(style.size))
* f64::from(gtk::pango::SCALE))
.round()
.clamp(0.0, f64::from(i32::MAX)) as i32;
layout.set_spacing(target_line_height.saturating_sub(natural_line_height));
}
layout.set_alignment(match style.horizontal_align {
HorizontalAlign::Start => gtk::pango::Alignment::Left,
HorizontalAlign::Center => gtk::pango::Alignment::Center,
HorizontalAlign::End => gtk::pango::Alignment::Right,
});
layout.set_wrap(gtk::pango::WrapMode::WordChar);
if let Some(bounds) = bounds.filter(|_| pango_layout_constrains_width(style)) {
layout.set_width(bounds.width.max(0).saturating_mul(gtk::pango::SCALE));
if style.wrap == TextWrap::Word {
layout.set_height(bounds.height.max(0).saturating_mul(gtk::pango::SCALE));
}
}
layout.set_single_paragraph_mode(style.wrap == TextWrap::NoWrap);
layout.set_ellipsize(if style.ellipsis {
gtk::pango::EllipsizeMode::End
} else {
gtk::pango::EllipsizeMode::None
});
}
fn pango_layout_constrains_width(style: &TextStyle) -> bool {
style.wrap == TextWrap::Word || style.ellipsis
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pango_no_wrap_text_is_not_given_a_wrapping_width() {
let mut style = TextStyle::line("Sans", 14.0, Color::rgb(0, 0, 0));
style.ellipsis = false;
assert!(!pango_layout_constrains_width(&style));
style.ellipsis = true;
assert!(pango_layout_constrains_width(&style));
style.ellipsis = false;
style.wrap = TextWrap::Word;
assert!(pango_layout_constrains_width(&style));
}
}