ltk/draw/
chrome.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Surface chrome: the client-side title bar and the Wayland input
//! region. Both are backend-agnostic — the software and GPU draw
//! paths call them with a `&mut Canvas` / a `&WlSurface` and get back
//! the geometry they need for hit testing + commit.

use smithay_client_toolkit::compositor::{ CompositorState, Region };
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;

use crate::render::Canvas;
use crate::types::{ Color, Rect };

/// Paint the client-side title bar (close button, divider, title text).
///
/// Returns the close button rect in physical pixels so the caller can
/// store it for hit testing. Returns `Rect::default()` when `tb_h <=
/// 0` — overlays / layer-shell surfaces have no title bar.
pub( crate ) fn draw_titlebar( canvas: &mut Canvas, title: &str, pw: u32, tb_h: f32, sf: f32 ) -> Rect
{
	if tb_h <= 0.0 { return Rect::default(); }
	let tb_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: tb_h };
	canvas.fill_rect( tb_rect, Color::rgb( 0.15, 0.15, 0.18 ), 0.0 );

	let title_size = 15.0;
	// `Canvas::draw_text` interprets `y` as the baseline. Match the
	// vertical-centring formula used by `Button::draw_text_button` so
	// chrome and content text lines line up at the same optical height.
	let title_y    = ( tb_h + title_size * sf ) / 2.0 - 2.0;
	let title_w    = canvas.measure_text( title, title_size );
	let title_x    = ( pw as f32 - title_w ) / 2.0;
	canvas.draw_text( title, title_x, title_y, title_size, Color::WHITE );

	let btn_size = tb_h - 8.0 * sf;
	let btn_x    = pw as f32 - btn_size - 8.0 * sf;
	let btn_y    = 4.0 * sf;
	let close_rect_phys = Rect { x: btn_x, y: btn_y, width: btn_size, height: btn_size };
	canvas.fill_rect( close_rect_phys, Color::rgba( 1.0, 1.0, 1.0, 0.1 ), 4.0 * sf );

	let cx  = btn_x + btn_size / 2.0;
	let cy  = btn_y + btn_size / 2.0;
	let arm = btn_size * 0.25;
	canvas.draw_line( cx - arm, cy - arm, cx + arm, cy + arm, Color::WHITE, 2.0 * sf );
	canvas.draw_line( cx + arm, cy - arm, cx - arm, cy + arm, Color::WHITE, 2.0 * sf );

	canvas.draw_line( 0.0, tb_h - 0.5, pw as f32, tb_h - 0.5, Color::rgba( 1.0, 1.0, 1.0, 0.15 ), 1.0 );

	close_rect_phys
}

/// Stamp a red warning banner at the top of the surface when the
/// active theme came from the in-memory B/W fallback (i.e.
/// [`crate::theme::is_fallback_active`] returned `true`). Gives the
/// user a clear visual cue that `ltk-theme-default` is missing.
///
/// Paints a thin red strip across the top of the surface (24 px
/// logical) with the install-instruction text. When the fallback is
/// not active this is a no-op.
pub( crate ) fn draw_fallback_banner(
	canvas: &mut Canvas,
	pw:     u32,
	sf:     f32,
)
{
	if !crate::theme::is_fallback_active() { return; }

	let banner_h = 24.0 * sf;
	let rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: banner_h };
	canvas.fill_rect( rect, Color::rgb( 0.75, 0.10, 0.10 ), 0.0 );

	let msg  = "ltk: fallback theme — install `ltk-theme-default`";
	let size = 12.0;
	let msg_w = canvas.measure_text( msg, size );
	let x     = ( ( pw as f32 - msg_w ) / 2.0 ).max( 6.0 * sf );
	let y     = ( banner_h - size * sf ) / 2.0;
	canvas.draw_text( msg, x, y, size, Color::WHITE );
}

/// Apply or clear the surface's input region (set by
/// `App::input_region`). `None` restores the default of "receive
/// input everywhere"; `Some(&[])` makes the surface pointer-through.
pub( crate ) fn apply_input_region(
	wl_surface:   &WlSurface,
	compositor:   &CompositorState,
	input_region: Option<&[Rect]>,
	scale:        u32,
)
{
	if let Some( regions ) = input_region
	{
		if let Ok( region ) = Region::new( compositor )
		{
			// `Rect`s are physical (layout space); `wl_region` is logical.
			let s = scale.max( 1 ) as f32;
			for r in regions
			{
				region.add(
					( r.x      / s ).round() as i32,
					( r.y      / s ).round() as i32,
					( r.width  / s ).round() as i32,
					( r.height / s ).round() as i32,
				);
			}
			wl_surface.set_input_region( Some( region.wl_region() ) );
		}
	} else {
		wl_surface.set_input_region( None );
	}
}