use crate::graphics::{Color, FontDesc, GraphicsContext, Rect, TextLayout};
use crate::ui::{Component, ComponentExt};
use anyhow::{Context, Result};
use x11rb::connection::Connection;
use x11rb::protocol::shape;
use x11rb::protocol::xproto::ConnectionExt as XprotoConnectionExt;
use x11rb::protocol::xproto::*;
use x11rb::protocol::Event;
use x11rb::wrapper::ConnectionExt;
pub struct Overlay {
conn: x11rb::xcb_ffi::XCBConnection,
window: Window,
screen: Screen,
graphics: Option<GraphicsContext>,
visual: Option<Visualtype>,
components: Vec<Box<dyn Component>>,
needs_redraw: bool,
}
impl Overlay {
pub fn new() -> Result<Self> {
let (conn, screen_num) = x11rb::xcb_ffi::XCBConnection::connect(None)
.context("Failed to connect to X11 server")?;
let setup = conn.setup();
let screen = setup.roots[screen_num].clone();
let window = conn.generate_id()?;
Ok(Self {
conn,
window,
screen,
graphics: None,
visual: None,
components: Vec::new(),
needs_redraw: true,
})
}
#[allow(dead_code)]
pub fn add_component(&mut self, component: Box<dyn Component>) {
self.components.push(component);
self.needs_redraw = true;
}
#[allow(dead_code)]
pub fn add_components(&mut self, components: impl IntoIterator<Item = Box<dyn Component>>) {
self.components.extend(components);
self.needs_redraw = true;
}
#[allow(dead_code)]
pub fn remove_components_where<F>(&mut self, predicate: F)
where
F: Fn(&Box<dyn Component>) -> bool,
{
let original_len = self.components.len();
self.components.retain(|component| !predicate(component));
if self.components.len() != original_len {
self.needs_redraw = true;
}
}
#[allow(dead_code)]
pub fn components_in_bounds(
&self,
bounds: crate::graphics::Rectangle,
) -> impl Iterator<Item = &Box<dyn Component>> {
self.components
.iter()
.filter(move |component| component.bounds().intersects(&bounds))
}
pub fn screen_width(&self) -> u16 {
self.screen.width_in_pixels
}
pub fn screen_height(&self) -> u16 {
self.screen.height_in_pixels
}
pub fn run(&mut self) -> Result<()> {
self.create_window()?;
self.configure_window_properties()?;
self.setup_click_through()?;
self.setup_graphics()?;
self.map_window()?;
println!("Overlay window created and mapped");
self.event_loop()
}
fn create_window(&mut self) -> Result<()> {
let visual = self
.find_argb_visual()
.context("Failed to find suitable visual for overlay")?;
self.visual = Some(visual);
let depth = self
.screen
.allowed_depths
.iter()
.find(|d| d.visuals.iter().any(|v| v.visual_id == visual.visual_id))
.map(|d| d.depth)
.unwrap_or(32);
println!(
"Using depth: {} for visual ID: 0x{:x}",
depth, visual.visual_id
);
let colormap = self.conn.generate_id()?;
self.conn
.create_colormap(
ColormapAlloc::NONE,
colormap,
self.screen.root,
visual.visual_id,
)
.context("Failed to create colormap")?;
let window_aux = CreateWindowAux::new()
.background_pixel(0)
.border_pixel(0)
.colormap(colormap)
.override_redirect(1) .event_mask(EventMask::EXPOSURE | EventMask::STRUCTURE_NOTIFY);
self.conn
.create_window(
depth,
self.window,
self.screen.root,
0,
0, self.screen.width_in_pixels,
self.screen.height_in_pixels,
0, WindowClass::INPUT_OUTPUT,
visual.visual_id,
&window_aux,
)
.context("Failed to create overlay window")?;
Ok(())
}
fn setup_graphics(&mut self) -> Result<()> {
if let Some(visual) = self.visual {
let conn_ptr = &self.conn as *const _ as *mut x11rb::xcb_ffi::XCBConnection;
let conn_static = unsafe { &*conn_ptr };
self.graphics = Some(GraphicsContext::new(
conn_static,
self.window,
&visual,
self.screen.width_in_pixels as i32,
self.screen.height_in_pixels as i32,
)?);
}
Ok(())
}
fn find_argb_visual(&self) -> Option<Visualtype> {
if let Some(visual) = self
.screen
.allowed_depths
.iter()
.filter(|depth| depth.depth == 32)
.flat_map(|depth| &depth.visuals)
.filter(|visual| visual.class == VisualClass::TRUE_COLOR)
.find(|visual| visual.visual_id >= 0x7a)
.copied()
{
println!("Selected safe 32-bit visual: ID=0x{:x}", visual.visual_id);
return Some(visual);
}
println!("No suitable visual found");
None
}
fn configure_window_properties(&self) -> Result<()> {
let class_name = b"x11-overlay\0x11-overlay\0";
self.conn
.change_property8(
PropMode::REPLACE,
self.window,
AtomEnum::WM_CLASS,
AtomEnum::STRING,
class_name,
)
.context("Failed to set window class property")?;
let title = b"x11-overlay";
self.conn
.change_property8(
PropMode::REPLACE,
self.window,
AtomEnum::WM_NAME,
AtomEnum::STRING,
title,
)
.context("Failed to set window title property")?;
self.set_ewmh_properties()?;
Ok(())
}
fn set_ewmh_properties(&self) -> Result<()> {
let net_wm_state = self.conn.intern_atom(false, b"_NET_WM_STATE")?;
let net_wm_state_above = self.conn.intern_atom(false, b"_NET_WM_STATE_ABOVE")?;
let net_wm_state_sticky = self.conn.intern_atom(false, b"_NET_WM_STATE_STICKY")?;
let net_wm_state_skip_taskbar = self
.conn
.intern_atom(false, b"_NET_WM_STATE_SKIP_TASKBAR")?;
let net_wm_window_type = self.conn.intern_atom(false, b"_NET_WM_WINDOW_TYPE")?;
let net_wm_window_type_desktop = self
.conn
.intern_atom(false, b"_NET_WM_WINDOW_TYPE_DESKTOP")?;
let net_wm_state = net_wm_state.reply()?.atom;
let net_wm_state_above = net_wm_state_above.reply()?.atom;
let net_wm_state_sticky = net_wm_state_sticky.reply()?.atom;
let net_wm_state_skip_taskbar = net_wm_state_skip_taskbar.reply()?.atom;
let net_wm_window_type = net_wm_window_type.reply()?.atom;
let net_wm_window_type_desktop = net_wm_window_type_desktop.reply()?.atom;
let states = [
net_wm_state_above,
net_wm_state_sticky,
net_wm_state_skip_taskbar,
];
self.conn
.change_property32(
PropMode::REPLACE,
self.window,
net_wm_state,
AtomEnum::ATOM,
&states,
)
.context("Failed to set EWMH window state properties")?;
self.conn
.change_property32(
PropMode::REPLACE,
self.window,
net_wm_window_type,
AtomEnum::ATOM,
&[net_wm_window_type_desktop],
)
.context("Failed to set EWMH window type property")?;
Ok(())
}
fn setup_click_through(&self) -> Result<()> {
let shape_rectangles: &[Rectangle] = &[];
shape::rectangles(
&self.conn,
shape::SO::SET,
shape::SK::INPUT,
ClipOrdering::UNSORTED,
self.window,
0,
0, shape_rectangles,
)
.context("Failed to setup click-through with XShape extension")?;
Ok(())
}
fn map_window(&self) -> Result<()> {
self.conn
.map_window(self.window)
.context("Failed to map overlay window")?;
self.conn.flush().context("Failed to flush X11 commands")?;
Ok(())
}
fn event_loop(&mut self) -> Result<()> {
let mut last_update = std::time::Instant::now();
self.needs_redraw = true;
loop {
while let Ok(Some(event)) = self.conn.poll_for_event() {
if let Event::Expose(_) = event {
self.needs_redraw = true;
}
}
let now = std::time::Instant::now();
let delta_time = (now - last_update).as_secs_f64();
last_update = now;
if self.components.update_all(delta_time) {
self.needs_redraw = true;
}
let removed_count = self.components.remove_expired();
if removed_count > 0 {
self.needs_redraw = true;
}
if self.needs_redraw {
self.render()?;
self.needs_redraw = false;
}
std::thread::sleep(std::time::Duration::from_millis(16)); }
}
fn render(&mut self) -> Result<()> {
let _screen_width = self.screen_width();
let _screen_height = self.screen_height();
if let Some(ref mut graphics) = self.graphics {
graphics.clear()?;
if graphics.has_cairo_surface() {
match graphics.get_cairo_context() {
Ok(Some(cairo_context)) => {
cairo_context.save().unwrap();
cairo_context.set_operator(cairo::Operator::Clear);
cairo_context.paint().unwrap();
cairo_context.restore().unwrap();
match self
.components
.iter()
.try_for_each(|component| component.render(graphics))
{
Ok(_) => {
}
Err(_) => {
Self::render_fallback_graphics(graphics)?;
}
}
}
Ok(None) | Err(_) => {
Self::render_fallback_graphics(graphics)?;
}
}
} else {
Self::render_fallback_graphics(graphics)?;
}
graphics.flush()
} else {
self.render_transparent_background()
}
}
fn render_transparent_background(&mut self) -> Result<()> {
if let Some(ref mut graphics) = self.graphics {
graphics.clear()?;
let font = FontDesc::new("DejaVu Sans", 24.0);
let layout = TextLayout::new()
.text("Hello from X11 Overlay!")
.font(font)
.color(Color::rgba(1.0, 1.0, 1.0, 0.9))
.bounds(Rect::new(50, 50, 400, 100))
.alignment(crate::graphics::Alignment::Center);
layout.render(graphics.text_renderer(), 50, 80)?;
graphics.copy_text_to_window()?;
graphics.flush()?;
} else {
self.conn
.clear_area(
false, self.window,
0,
0, self.screen.width_in_pixels,
self.screen.height_in_pixels,
)
.context("Failed to clear window area for transparency")?;
self.conn
.flush()
.context("Failed to flush rendering commands")?;
}
Ok(())
}
fn render_fallback_graphics(graphics: &mut GraphicsContext) -> Result<()> {
graphics.fill_rectangle(Rect::new(50, 50, 400, 50), Color::rgba(1.0, 0.3, 0.3, 0.9))?;
graphics.fill_rectangle(
Rect::new(graphics.width() / 2 - 200, 150, 400, 80),
Color::rgba(0.3, 0.3, 1.0, 0.9),
)?;
graphics.fill_rectangle(
Rect::new(100, 300, 350, 60),
Color::rgba(0.3, 1.0, 0.3, 0.9),
)?;
graphics.fill_rectangle(
Rect::new(200, 450, 500, 120),
Color::rgba(1.0, 1.0, 0.3, 0.9),
)?;
graphics.fill_rectangle(
Rect::new(graphics.width() - 350, graphics.height() - 200, 300, 150),
Color::rgba(1.0, 0.3, 1.0, 0.7),
)?;
graphics.fill_rectangle(
Rect::new(10, 10, 20, 20),
Color::rgba(1.0, 0.5, 0.0, 1.0), )?;
Ok(())
}
}