First commit. Version 0.1.0

This commit is contained in:
2026-05-10 09:58:23 +02:00
parent af105b7f7d
commit bbab5e238d
635 changed files with 53627 additions and 175 deletions

253
src/draw/gles.rs Normal file
View File

@@ -0,0 +1,253 @@
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! GLES (EGL + FBO) draw paths.
//!
//! The GPU counterparts to [`super::software::draw_surface_full`] and
//! [`super::software::draw_surface_partial`]. Same DrawCtx flow; the
//! differences are:
//!
//! * Canvas is `Canvas::new_gles` backed by a persistent FBO rather
//! than a CPU pixmap → FBO pixels survive across frames naturally,
//! so the partial path only needs a scissor.
//! * Presentation goes through `egl_ctx.swap_buffers_with_damage`
//! which attaches + damages + commits atomically. The partial path
//! translates the top-left dirty rects into EGL's bottom-left
//! convention before passing them in.
//! * No `wl_shm` pool; no `pick_shm_format`; no byte-order swap.
//!
//! The Y-flip for the swap-with-damage call lives here (only the GPU
//! path needs it — SHM buffers are already top-down).
use std::sync::Arc;
use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use crate::egl_context::EglContext;
use crate::event_loop::SurfaceState;
use crate::render::Canvas;
use crate::types::{ Color, Rect };
use crate::widget::Element;
use super::{ DrawCtx, layout_and_draw };
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar };
/// GPU full-redraw path. Mirrors [`super::software::draw_surface_full`]
/// but writes into the EGL surface's persistent FBO instead of an SHM
/// buffer:
///
/// 1. `eglMakeCurrent` so subsequent GL calls hit this surface.
/// 2. (Lazily) build the [`Canvas::Gles`], or resize its FBO if the
/// surface dimensions changed.
/// 3. Clear, run layout+draw, blit the FBO to the default framebuffer.
/// 4. Set buffer_scale + input_region (state attached to the implicit
/// commit done by `eglSwapBuffers`).
/// 5. `eglSwapBuffers` — performs the wl attach/damage/commit atomically.
pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
ss: &mut SurfaceState<Msg>,
compositor: &CompositorState,
egl_ctx: &Arc<EglContext>,
view: &Element<Msg>,
bg: Color,
input_region: Option<&[Rect]>,
debug_layout: bool,
pw: u32,
ph: u32,
scale: u32,
request_frame: &dyn Fn( &WlSurface ),
)
{
let Some( es ) = ss.egl_surface.as_ref() else { return };
if egl_ctx.make_current( es ).is_err() { return; }
let canvas = ss.canvas.get_or_insert_with( ||
{
let mut c = Canvas::new_gles(
Arc::clone( egl_ctx.gl() ), egl_ctx.version, pw, ph,
);
c.set_dpi_scale( scale as f32 );
if let Some( reg ) = crate::theme::build_font_registry()
{
c.set_font_registry( Arc::new( reg ) );
}
c
} );
if canvas.size() != ( pw, ph )
{
canvas.resize( pw, ph );
canvas.set_dpi_scale( scale as f32 );
}
canvas.clear_clip();
let sf = scale as f32;
let tb_h = ss.titlebar_height * sf;
let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: ( ph as f32 - tb_h ).max( 0.0 ) };
if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear(); }
ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf );
let mut ctx: DrawCtx<Msg> = DrawCtx
{
focused_idx: ss.focused_idx,
hovered_idx: ss.hovered_idx,
pressed_idx: ss.gesture.pressed_idx,
cursor_state: std::mem::take( &mut ss.cursor_state ),
selection_anchor: std::mem::take( &mut ss.selection_anchor ),
widget_rects: Vec::new(),
debug_layout,
scroll_offsets: std::mem::take( &mut ss.scroll_offsets ),
scroll_rects: Vec::new(),
scroll_canvases: std::mem::take( &mut ss.scroll_canvases ),
scroll_navigable_items: std::mem::take( &mut ss.scroll_navigable_items ),
previous_widget_rects: ss.widget_rects.clone(),
};
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
if ctx.debug_layout
{
for w in &ctx.widget_rects
{
canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 );
}
}
if let Some( ref menu ) = ss.context_menu
{
super::draw_context_menu( canvas, menu );
}
// Fallback-theme warning. Painted last so it sits on top of every
// widget; no-op when the real theme is loaded.
draw_fallback_banner( canvas, pw, sf );
canvas.present();
ss.prev_focused = ss.focused_idx;
ss.prev_hovered = ss.hovered_idx;
ss.prev_pressed = ss.gesture.pressed_idx;
ss.widget_rects = ctx.widget_rects;
ss.scroll_rects = ctx.scroll_rects;
ss.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases );
ss.cursor_state = ctx.cursor_state;
ss.selection_anchor = ctx.selection_anchor;
ss.scroll_offsets = ctx.scroll_offsets;
ss.scroll_navigable_items = ctx.scroll_navigable_items;
ss.content_dirty = false;
let wl_surface = ss.surface.wl_surface();
apply_input_region( wl_surface, compositor, input_region );
// Frame callback request must precede the implicit commit done by
// `swap_buffers_with_damage`, so the compositor can attach it to this
// frame and not the previous one.
request_frame( wl_surface );
// Full-surface damage. (0,0,w,h) is identical in EGL bottom-left and
// top-left coords, so no Y-flip needed.
let _ = egl_ctx.swap_buffers_with_damage( es, &[ ( 0, 0, pw as i32, ph as i32 ) ] );
ss.frame_pending = true;
}
/// GPU partial-redraw path. Same flow as
/// [`super::software::draw_surface_partial`] (clip mask → repaint
/// background + widgets → present), but the clip is implemented with
/// `glScissor` (bbox union) inside the canvas, and presentation goes
/// through `eglSwapBuffers`. The FBO is preserved between frames so
/// the unclipped pixels naturally carry over.
pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
ss: &mut SurfaceState<Msg>,
compositor: &CompositorState,
egl_ctx: &Arc<EglContext>,
view: &Element<Msg>,
bg: Color,
input_region: Option<&[Rect]>,
dirty_rects: Vec<Rect>,
pw: u32,
ph: u32,
scale: u32,
request_frame: &dyn Fn( &WlSurface ),
)
{
let Some( es ) = ss.egl_surface.as_ref() else { return };
if egl_ctx.make_current( es ).is_err() { return; }
let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" );
if canvas.size() != ( pw, ph )
{
canvas.resize( pw, ph );
canvas.set_dpi_scale( scale as f32 );
}
canvas.set_clip_rects( &dirty_rects );
let sf = scale as f32;
let tb_h = ss.titlebar_height * sf;
let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: ( ph as f32 - tb_h ).max( 0.0 ) };
if bg.a > 0.0 { canvas.fill( bg ); }
else
{
canvas.clear_rects_transparent( &dirty_rects );
}
ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf );
let mut ctx: DrawCtx<Msg> = DrawCtx
{
focused_idx: ss.focused_idx,
hovered_idx: ss.hovered_idx,
pressed_idx: ss.gesture.pressed_idx,
cursor_state: std::mem::take( &mut ss.cursor_state ),
selection_anchor: std::mem::take( &mut ss.selection_anchor ),
widget_rects: Vec::new(),
debug_layout: false,
scroll_offsets: std::mem::take( &mut ss.scroll_offsets ),
scroll_rects: Vec::new(),
scroll_canvases: std::mem::take( &mut ss.scroll_canvases ),
scroll_navigable_items: std::mem::take( &mut ss.scroll_navigable_items ),
previous_widget_rects: ss.widget_rects.clone(),
};
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
if let Some( ref menu ) = ss.context_menu
{
super::draw_context_menu( canvas, menu );
}
// Fallback-theme warning. Painted last so it sits on top of every
// widget; no-op when the real theme is loaded.
draw_fallback_banner( canvas, pw, sf );
canvas.clear_clip();
canvas.present();
ss.prev_focused = ss.focused_idx;
ss.prev_hovered = ss.hovered_idx;
ss.prev_pressed = ss.gesture.pressed_idx;
ss.widget_rects = ctx.widget_rects;
ss.scroll_rects = ctx.scroll_rects;
ss.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases );
ss.cursor_state = ctx.cursor_state;
ss.selection_anchor = ctx.selection_anchor;
ss.scroll_offsets = ctx.scroll_offsets;
ss.scroll_navigable_items = ctx.scroll_navigable_items;
ss.content_dirty = false;
let wl_surface = ss.surface.wl_surface();
apply_input_region( wl_surface, compositor, input_region );
request_frame( wl_surface );
// Convert top-left dirty rects to EGL bottom-left coords for the
// swap-with-damage call.
let damage: Vec<( i32, i32, i32, i32 )> = dirty_rects.iter().map( |r|
{
let x = r.x.floor() as i32;
let w = r.width.ceil() as i32;
let h = r.height.ceil() as i32;
let y_top = r.y.floor() as i32;
let y_bottom = ph as i32 - y_top - h;
( x, y_bottom.max( 0 ), w.max( 0 ), h.max( 0 ) )
} ).collect();
let _ = egl_ctx.swap_buffers_with_damage( es, &damage );
ss.frame_pending = true;
}