ltk/platform/wayland/
window_fit.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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Keeps [`App::window_size_hint`](crate::App::window_size_hint) inside the screen.

use smithay_client_toolkit::reexports::client::protocol::wl_output::WlOutput;
use smithay_client_toolkit::shell::xdg::window::Window;

use crate::app::App;
use crate::event_loop::AppData;

/// Share of an output a window may take when the compositor sent no `configure_bounds`.
pub( crate ) const OUTPUT_SHARE: f32 = 0.9;

/// The part of an output of logical size `output` a window may take.
pub( crate ) fn output_share( output: ( u32, u32 ) ) -> ( u32, u32 )
{
	( ( output.0 as f32 * OUTPUT_SHARE ) as u32, ( output.1 as f32 * OUTPUT_SHARE ) as u32 )
}

/// `size` scaled down, keeping its aspect ratio, until it fits in `bounds`.
pub( crate ) fn fit_within( size: ( u32, u32 ), bounds: ( u32, u32 ) ) -> ( u32, u32 )
{
	let ( w, h )   = size;
	let ( bw, bh ) = bounds;
	if w == 0 || h == 0 || bw == 0 || bh == 0 { return size; }
	if w <= bw && h <= bh { return size; }
	let scale = ( bw as f32 / w as f32 ).min( bh as f32 / h as f32 );
	( ( ( w as f32 * scale ) as u32 ).max( 1 ), ( ( h as f32 * scale ) as u32 ).max( 1 ) )
}

impl<A: App> AppData<A>
{
	/// On the first configure, shrink a size hint larger than the screen. Returns whether it did.
	pub( crate ) fn fit_size_hint( &mut self, window: &Window, suggested: Option<( u32, u32 )> ) -> bool
	{
		if !std::mem::take( &mut self.platform.pending_size_hint_fit ) { return false; }
		let Some( hint ) = self.app.window_size_hint() else { return false };
		let bounds = suggested.filter( |b| b.0 > 0 && b.1 > 0 ).or_else( || self.output_bounds() );
		let Some( bounds ) = bounds else { return false };
		let fitted = fit_within( hint, bounds );
		if fitted == hint { return false; }
		window.set_min_size( Some( fitted ) );
		if !self.app.window_resizable() { window.set_max_size( Some( fitted ) ); }
		self.platform.fitted_size_hint = Some( fitted );
		true
	}

	fn output_bounds( &self ) -> Option<( u32, u32 )>
	{
		self.output_logical_size( None ).map( output_share )
	}

	fn output_logical_size( &self, preferred: Option<&WlOutput> ) -> Option<( u32, u32 )>
	{
		let info = preferred
			.or( self.platform.main_output.as_ref() )
			.and_then( |o| self.platform.output_state.info( o ) )
			.or_else( || self.platform.output_state.outputs().find_map( |o| self.platform.output_state.info( &o ) ) )?;
		let ( w, h ) = info.logical_size.or_else( ||
		{
			let mode  = info.modes.iter().find( |m| m.current )?;
			let scale = info.scale_factor.max( 1 );
			Some( ( mode.dimensions.0 / scale, mode.dimensions.1 / scale ) )
		} )?;
		( w > 0 && h > 0 ).then_some( ( w as u32, h as u32 ) )
	}

	pub( crate ) fn publish_output_size( &self, preferred: Option<&WlOutput> )
	{
		let ( w, h ) = self.output_logical_size( preferred ).unwrap_or( ( 0, 0 ) );
		crate::types::set_output_logical_size( w, h );
	}
}

#[ cfg( test ) ]
mod tests
{
	use super::*;

	#[ test ]
	fn a_size_that_fits_is_kept()
	{
		assert_eq!( fit_within( ( 800, 600 ), ( 1920, 1080 ) ), ( 800, 600 ) );
		assert_eq!( fit_within( ( 1920, 1080 ), ( 1920, 1080 ) ), ( 1920, 1080 ) );
	}

	#[ test ]
	fn a_tall_window_shrinks_to_the_height_and_keeps_its_shape()
	{
		assert_eq!( fit_within( ( 720, 1440 ), ( 1920, 1080 ) ), ( 540, 1080 ) );
	}

	#[ test ]
	fn a_wide_window_shrinks_to_the_width()
	{
		assert_eq!( fit_within( ( 1600, 900 ), ( 800, 1280 ) ), ( 800, 450 ) );
	}

	#[ test ]
	fn degenerate_input_is_left_alone()
	{
		assert_eq!( fit_within( ( 720, 1440 ), ( 0, 1080 ) ), ( 720, 1440 ) );
		assert_eq!( fit_within( ( 0, 1440 ), ( 1920, 1080 ) ), ( 0, 1440 ) );
	}
}