WallpaperBundle::blurred; the on-screen keyboard at the right size on a scaled output, keys as tall as they are wide, a terminal row that fits and a Spanish symbols layer that does not squeeze the rest
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

The keyboard of the previous commit was drawn and tried on a desktop at scale 1. On the Librem 5, at scale 2 and hosted by forge, it showed what that could not: letters twice their size, a terminal row whose modifier labels did not fit, keys shorter than they were wide, and a Spanish numbers layer with a gap at each side. This fixes those, and adds the wallpaper helper the shell and the compositor now use for their frosted panels.
A frosted wallpaper. Crustace decoded the theme's lockscreen image at full size a second time for the launcher and the quick settings, and forge decoded it again for the app switcher, blurred it with a floating-point Gaussian at 1920 px and composed a copy per panel size; the switcher's decode alone left some 60 MB in a thread arena that glibc never gives back. A backdrop that is blurred needs no resolution. `WallpaperBundle::blurred( divisor, sigma )` returns a bundle `divisor` times smaller on each side, reduced with a triangle filter and blurred with `imageops::fast_blur`, the box approximation that works in place on the 8-bit pixels; the renderer scales it back up with linear filtering, and the blur hides what the reduction lost. It is an ordinary bundle, so `for_size` crops it for portrait as it does the original. Two tests: the size is divided and never reaches zero, and a flat colour stays that colour. `docs/architecture.md` mentions it next to `for_size`.
Text twice its size. `Panel::draw` passed the canvas a font size already multiplied by the output scale, and the canvas multiplies every font size by its `dpi_scale` once more, as it must for widget text, which is sized in font space. At scale 1 the two agree; at 2 the labels came out at four times the area. The keys and the variants popup now pass `TEXT_SIZE` and `ACTION_SIZE` unscaled, and `draw_centred` computes the baseline offset from the size in physical pixels, `size * dpi_scale()`, so the label stays centred on the key. Radii, key geometry and icons were already right: they are physical pixels and never go through `dpi_scale`. The runtime's built-in panel and forge's both draw through `Panel::draw`, so both had it.
Keys as tall as they are wide. Rows were a fixed 46 px high whatever the width, so on a phone the keys came out wide and flat. The new `key_unit( width, scale )` is the width one key unit takes when the widest row of the layer fills the panel, the width of a `q`; `set_rect` uses it as both the width unit and the height of every row, the terminal row included, and the variants popup, which was already as tall as its key, follows. The panel's height therefore depends on its width: `height( scale )` becomes `height( width, scale )`, `place_osk` passes the surface width, and `KEY_HEIGHT` goes. The architecture document says so. In landscape the keys grow with the screen's width; a cap on the height has not been needed yet.
The terminal row. Esc, Tab, Ctrl and Alt were one unit wide and their labels did not fit. They are 1.5 units now, and to keep the row at 13 units `\` and `~` leave it: the row is Esc, Tab, Ctrl, Alt, the four arrows, `-`, `/` and `|`. Neither is lost: `\` is a long press on `/`, next to `?`, and `~` one on `-`, next to `_` and `–`; `|` takes over the variants `\` had, `` ` ``, `^`, `$`, `#` and `*`, beside its own `&` and `!`. The arrows are drawn at 14 instead of the 22 of the other action icons, which filled the narrow key. The built-in bar's test checks `|` in the row and `\` and `~` among the variants.
The Spanish symbols layer. Its third row carried `¡`, `!`, `¿` and `?`, 12 units against the 10 of every other row; since all rows share the key unit of the widest, the numbers and `@ # € %` rows came out narrower and centred, with a gap at each side, and switching to English and back looked right only because that layout's row has 10. `¡` and `¿` leave the row and become long presses on `!` and `?`, as on Android, and stay among the variants of `.` where they already were. The new test `shipped_layers_are_no_wider_than_their_base` reads `en.json` and `es.json` and fails when any row of any layer is wider than the widest row of the base layer, so a layout file cannot bring this back unnoticed.
This commit is contained in:
2026-09-27 21:43:33 +02:00
parent a1a4d51677
commit a0ceef51ef
8 changed files with 106 additions and 37 deletions
+2 -2
View File
@@ -170,7 +170,7 @@ Text entry on a phone comes from `zwp_text_input_v3`: a focused `TextEdit` enabl
A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert` and `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. Those are what `keyboards/bar-terminal.json` is made of: the row a terminal needs, independent of the language, which the panel lays above the language's layout when its `Purpose` is `Terminal`. A `bar-*.json` file is a row, not a layout, and `available()` leaves it out of the layouts the layout-switch key cycles through. A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert` and `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. Those are what `keyboards/bar-terminal.json` is made of: the row a terminal needs, independent of the language, which the panel lays above the language's layout when its `Purpose` is `Terminal`. A `bar-*.json` file is a row, not a layout, and `available()` leaves it out of the layouts the layout-switch key cycles through.
**`Panel` is a state machine a host drives.** It holds the loaded layouts, the current layer, one-shot or locked shift, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event` the host acts on: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's own look changed. `set_purpose` switches the extra row; the runtime derives the purpose from the focused field, and a compositor takes it from the `content_type` a client sets on `zwp_text_input_v3`, which has a `terminal` purpose that terminal emulators built on VTE declare; for one that does not, the compositor can decide by `app_id`. `next_deadline()` says when `tick` next has work: the variants popup opens 450 ms into a press, backspace repeats after the same delay. `height( scale )` is what the panel needs for its current layer; `draw( canvas )` paints it with the active palette. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it. **`Panel` is a state machine a host drives.** It holds the loaded layouts, the current layer, one-shot or locked shift, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event` the host acts on: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's own look changed. `set_purpose` switches the extra row; the runtime derives the purpose from the focused field, and a compositor takes it from the `content_type` a client sets on `zwp_text_input_v3`, which has a `terminal` purpose that terminal emulators built on VTE declare; for one that does not, the compositor can decide by `app_id`. `next_deadline()` says when `tick` next has work: the variants popup opens 450 ms into a press, backspace repeats after the same delay. `height( width, scale )` is what the panel needs at that width for its current layer, since keys are as tall as one key unit is wide; `draw( canvas )` paints it with the active palette. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it.
**The runtime shows it under a focused text field** when the compositor advertises no `zwp_text_input_manager_v3`, when `LTK_BUILTIN_OSK=1` is set (useful to try it on a desktop that has an input method), or when the app returns `true` from `builtin_keyboard()`. The panel sits at the bottom of the surface; the widget tree is laid out in the space above it, so the focused field is never covered, and presses inside it never reach the gesture machine. `Text` goes through the same insertion path as a physical key, `Key` through the same dispatch as one, with Ctrl applied for its duration, `Enter` submits the field or inserts a newline in a multiline one, `Hide` keeps the focus and takes the panel away until the field is tapped again; losing the focus removes it. **The runtime shows it under a focused text field** when the compositor advertises no `zwp_text_input_manager_v3`, when `LTK_BUILTIN_OSK=1` is set (useful to try it on a desktop that has an input method), or when the app returns `true` from `builtin_keyboard()`. The panel sits at the bottom of the surface; the widget tree is laid out in the space above it, so the focused field is never covered, and presses inside it never reach the gesture machine. `Text` goes through the same insertion path as a physical key, `Key` through the same dispatch as one, with Ctrl applied for its duration, `Enter` submits the field or inserts a newline in a multiline one, `Hide` keeps the focus and takes the panel away until the field is tapped again; losing the focus removes it.
@@ -209,7 +209,7 @@ For dev iteration set `LTK_THEMES_DIR=/path/to/ltk/themes` so the lookup picks f
2. `$XDG_DATA_HOME/ltk/themes/<id>/` (defaults to `~/.local/share/ltk/themes/<id>/`) 2. `$XDG_DATA_HOME/ltk/themes/<id>/` (defaults to `~/.local/share/ltk/themes/<id>/`)
3. `/usr/share/ltk/themes/<id>/` 3. `/usr/share/ltk/themes/<id>/`
Wallpapers ship as a single landscape PNG per variant. `ltk::WallpaperBundle::from_path_or_bytes( path, bundled_fallback )` handles the disk-or-builtin fallback, and `bundle.for_size( sw, sh )` returns the right crop for landscape *or* portrait surfaces — no need to ship two PNGs. Wallpapers ship as a single landscape PNG per variant. `ltk::WallpaperBundle::from_path_or_bytes( path, bundled_fallback )` handles the disk-or-builtin fallback, and `bundle.for_size( sw, sh )` returns the right crop for landscape *or* portrait surfaces — no need to ship two PNGs. A panel that wants a frosted version of the same wallpaper takes `bundle.blurred( divisor, sigma )`, a bundle `divisor` times smaller on each side and blurred, which the renderer scales back up: a fraction of the memory of a second image, and no second decode.
For many third-party apps, theming is optional at first. It is reasonable to For many third-party apps, theming is optional at first. It is reasonable to
start with the default theme and come back to the runtime theme APIs later as start with the default theme and come back to the runtime theme APIs later as
+8 -12
View File
@@ -4,27 +4,23 @@
"layers": { "layers": {
"base": [ "base": [
[ [
{ "key": "Escape", "label": "Esc" }, { "key": "Escape", "label": "Esc", "width": 1.5 },
{ "key": "Tab" }, { "key": "Tab", "width": 1.5 },
{ "modifier": "ctrl" }, { "modifier": "ctrl", "width": 1.5 },
{ "modifier": "alt" }, { "modifier": "alt", "width": 1.5 },
{ "key": "Up" }, { "key": "Up" },
{ "key": "Down" }, { "key": "Down" },
{ "key": "Left" }, { "key": "Left" },
{ "key": "Right" }, { "key": "Right" },
"-", "-",
"/", "/",
"|", "|"
"\\",
"~"
] ]
] ]
}, },
"variants": { "variants": {
"-": [ "_", "–" ], "-": [ "~", "_", "–" ],
"/": [ "?" ], "/": [ "\\", "?" ],
"|": [ "&", "!" ], "|": [ "&", "!", "`", "^", "$", "#", "*" ]
"\\": [ "`", "^" ],
"~": [ "$", "#", "*" ]
} }
} }
+4 -2
View File
@@ -11,7 +11,7 @@
"symbols": [ "symbols": [
[ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ], [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ],
[ "@", "#", "€", "%", "&", "-", "+", "(", ")", "/" ], [ "@", "#", "€", "%", "&", "-", "+", "(", ")", "/" ],
[ { "action": "layer", "layer": "more", "label": "=\\<", "width": 1.5 }, "*", "\"", "'", ":", ";", "¡", "!", "¿", "?", { "action": "backspace", "width": 1.5 } ], [ { "action": "layer", "layer": "more", "label": "=\\<", "width": 1.5 }, "*", "\"", "'", ":", ";", "!", "?", { "action": "backspace", "width": 1.5 } ],
[ { "action": "layer", "layer": "base", "label": "ABC", "width": 1.5 }, { "action": "layout", "width": 1 }, ",", { "action": "space", "width": 4 }, ".", { "action": "enter", "width": 1.5 } ] [ { "action": "layer", "layer": "base", "label": "ABC", "width": 1.5 }, { "action": "layout", "width": 1 }, ",", { "action": "space", "width": 4 }, ".", { "action": "enter", "width": 1.5 } ]
], ],
"more": [ "more": [
@@ -34,6 +34,8 @@
"-": [ "–", "—", "_" ], "-": [ "–", "—", "_" ],
"\"": [ "«", "»", "“", "”" ], "\"": [ "«", "»", "“", "”" ],
"'": [ "‘", "’" ], "'": [ "‘", "’" ],
"€": [ "$", "£", "¥" ] "€": [ "$", "£", "¥" ],
"!": [ "¡" ],
"?": [ "¿" ]
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ use crate::types::{ Point, Rect };
pub( crate ) fn place_osk( osk: &mut Option<Panel>, pw: u32, ph: u32, sf: f32 ) -> f32 pub( crate ) fn place_osk( osk: &mut Option<Panel>, pw: u32, ph: u32, sf: f32 ) -> f32
{ {
let Some( panel ) = osk.as_mut() else { return 0.0 }; let Some( panel ) = osk.as_mut() else { return 0.0 };
let h = panel.height( sf ).min( ph as f32 ); let h = panel.height( pw as f32, sf ).min( ph as f32 );
panel.set_rect( Rect { x: 0.0, y: ph as f32 - h, width: pw as f32, height: h }, sf ); panel.set_rect( Rect { x: 0.0, y: ph as f32 - h, width: pw as f32, height: h }, sf );
h h
} }
+8 -4
View File
@@ -11,6 +11,7 @@ use super::{ Action, Modifier, Panel };
const TEXT_SIZE: f32 = 20.0; const TEXT_SIZE: f32 = 20.0;
const ACTION_SIZE: f32 = 14.0; const ACTION_SIZE: f32 = 14.0;
const ICON_SIZE: f32 = 22.0; const ICON_SIZE: f32 = 22.0;
const ARROW_SIZE: f32 = 14.0;
const RADIUS: f32 = 6.0; const RADIUS: f32 = 6.0;
/// The theme icon a key shows; its label is the fallback when the theme lacks it. /// The theme icon a key shows; its label is the fallback when the theme lacks it.
@@ -54,7 +55,8 @@ impl Panel
let bg = if lit { palette.accent } else if action { palette.divider } else { palette.surface }; let bg = if lit { palette.accent } else if action { palette.divider } else { palette.surface };
let ink = if lit { palette.bg } else { palette.text_primary }; let ink = if lit { palette.bg } else { palette.text_primary };
canvas.fill_rect( rect, bg, RADIUS * scale ); canvas.fill_rect( rect, bg, RADIUS * scale );
let icon = icon_for( &key ).and_then( |name| self.icon( name, ( ICON_SIZE * scale ) as u32, ink ) ); let size = if key.keysym.is_some() { ARROW_SIZE } else { ICON_SIZE };
let icon = icon_for( &key ).and_then( |name| self.icon( name, ( size * scale ) as u32, ink ) );
match icon match icon
{ {
Some( ( rgba, w, h ) ) => Some( ( rgba, w, h ) ) =>
@@ -64,7 +66,7 @@ impl Panel
} }
None => None =>
{ {
let size = if action { ACTION_SIZE } else { TEXT_SIZE } * scale; let size = if action { ACTION_SIZE } else { TEXT_SIZE };
draw_centred( canvas, &key.label, rect, size, ink ); draw_centred( canvas, &key.label, rect, size, ink );
} }
} }
@@ -79,7 +81,7 @@ impl Panel
let bg = if lit { palette.accent } else { palette.surface }; let bg = if lit { palette.accent } else { palette.surface };
let ink = if lit { palette.bg } else { palette.text_primary }; let ink = if lit { palette.bg } else { palette.text_primary };
canvas.fill_rect( *rect, bg, RADIUS * scale ); canvas.fill_rect( *rect, bg, RADIUS * scale );
draw_centred( canvas, text, *rect, TEXT_SIZE * scale, ink ); draw_centred( canvas, text, *rect, TEXT_SIZE, ink );
} }
} }
} }
@@ -99,9 +101,11 @@ fn channel( v: f32 ) -> u8
( v.clamp( 0.0, 1.0 ) * 255.0 ).round() as u8 ( v.clamp( 0.0, 1.0 ) * 255.0 ).round() as u8
} }
/// `size` is in font space: the canvas multiplies it by its `dpi_scale`, as it does for every widget's text.
fn draw_centred( canvas: &mut Canvas, text: &str, rect: Rect, size: f32, color: Color ) fn draw_centred( canvas: &mut Canvas, text: &str, rect: Rect, size: f32, color: Color )
{ {
if text.is_empty() { return; } if text.is_empty() { return; }
let w = canvas.measure_text( text, size ); let w = canvas.measure_text( text, size );
canvas.draw_text( text, rect.x + ( rect.width - w ) / 2.0, rect.y + rect.height / 2.0 + size * 0.35, size, color ); let glyph_h = size * canvas.dpi_scale();
canvas.draw_text( text, rect.x + ( rect.width - w ) / 2.0, rect.y + rect.height / 2.0 + glyph_h * 0.35, size, color );
} }
+22
View File
@@ -352,6 +352,28 @@ mod tests
assert!( row.iter().any( |k| k.keysym == Some( Keysym::Escape ) ) ); assert!( row.iter().any( |k| k.keysym == Some( Keysym::Escape ) ) );
assert!( row.iter().any( |k| k.modifier == Some( Modifier::Ctrl ) ) ); assert!( row.iter().any( |k| k.modifier == Some( Modifier::Ctrl ) ) );
assert!( row.iter().any( |k| k.text.as_deref() == Some( "|" ) ) ); assert!( row.iter().any( |k| k.text.as_deref() == Some( "|" ) ) );
assert!( bar.variants( "/" ).iter().any( |v| v == "\\" ) );
assert!( bar.variants( "-" ).iter().any( |v| v == "~" ) );
}
/// Rows share the key unit of the widest one, so a layer wider than the
/// base narrows every key in it and leaves gaps at the sides.
#[ test ]
fn shipped_layers_are_no_wider_than_their_base()
{
let units = |row: &Vec<Key>| row.iter().map( |k| k.width ).sum::<f32>();
for json in [ include_str!( "../../keyboards/en.json" ), include_str!( "../../keyboards/es.json" ) ]
{
let layout = Layout::from_json( json ).unwrap();
let base = layout.rows( BASE_LAYER ).iter().map( units ).fold( 0.0_f32, f32::max );
for ( name, rows ) in &layout.layers
{
for row in rows
{
assert!( units( row ) <= base, "{}: layer {name} has a row of {} units, the base {base}", layout.name, units( row ) );
}
}
}
} }
#[ test ] #[ test ]
+24 -16
View File
@@ -21,7 +21,6 @@ pub const LONG_PRESS: Duration = Duration::from_millis( 450 );
const REPEAT_INTERVAL: Duration = Duration::from_millis( 60 ); const REPEAT_INTERVAL: Duration = Duration::from_millis( 60 );
const SHIFT_DOUBLE_TAP: Duration = Duration::from_millis( 350 ); const SHIFT_DOUBLE_TAP: Duration = Duration::from_millis( 350 );
/// Design sizes in logical pixels; the panel multiplies them by its scale. /// Design sizes in logical pixels; the panel multiplies them by its scale.
const KEY_HEIGHT: f32 = 46.0;
const GAP: f32 = 6.0; const GAP: f32 = 6.0;
const PADDING: f32 = 6.0; const PADDING: f32 = 6.0;
@@ -221,11 +220,24 @@ impl Panel
self.bar.as_ref().map( |b| b.rows( layout::BASE_LAYER ) ).unwrap_or( &[] ) self.bar.as_ref().map( |b| b.rows( layout::BASE_LAYER ) ).unwrap_or( &[] )
} }
/// Height the panel needs at `scale` for its current layer and purpose. /// Height the panel needs `width` pixels wide at `scale`, for its current layer and purpose.
pub fn height( &self, scale: f32 ) -> f32 pub fn height( &self, width: f32, scale: f32 ) -> f32
{ {
let rows = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32; let rows = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32;
( rows * KEY_HEIGHT + ( rows - 1.0 ).max( 0.0 ) * GAP + 2.0 * PADDING ) * scale rows * self.key_unit( width, scale ) + ( ( rows - 1.0 ).max( 0.0 ) * GAP + 2.0 * PADDING ) * scale
}
/// Width of one key unit of the layout's rows, `width` pixels wide: the
/// widest row fills the panel. Keys are as tall as one unit is wide.
fn key_unit( &self, width: f32, scale: f32 ) -> f32
{
let rows = self.layout().rows( &self.layer );
let widest = rows.iter()
.map( |row| row.iter().map( |k| k.width ).sum::<f32>() )
.fold( 0.0_f32, f32::max )
.max( 1.0 );
let most = rows.iter().map( Vec::len ).max().unwrap_or( 1 ).saturating_sub( 1 ) as f32;
( width - 2.0 * PADDING * scale - GAP * scale * most ) / widest
} }
/// Place the keys inside `rect`: the purpose's rows first, each spanning the width, then the layout's, which share one key unit and are centred when narrower than the widest. /// Place the keys inside `rect`: the purpose's rows first, each spanning the width, then the layout's, which share one key unit and are centred when narrower than the widest.
@@ -236,7 +248,7 @@ impl Panel
self.keys.clear(); self.keys.clear();
let gap = GAP * scale; let gap = GAP * scale;
let pad = PADDING * scale; let pad = PADDING * scale;
let key_h = KEY_HEIGHT * scale; let key_h = self.key_unit( rect.width, scale );
let avail = rect.width - 2.0 * pad; let avail = rect.width - 2.0 * pad;
let mut y = rect.y + pad; let mut y = rect.y + pad;
@@ -252,11 +264,7 @@ impl Panel
let rows: Vec<Vec<Key>> = self.layout().rows( &self.layer ).iter() let rows: Vec<Vec<Key>> = self.layout().rows( &self.layer ).iter()
.map( |row| row.iter().map( |k| self.labelled( k ) ).collect() ) .map( |row| row.iter().map( |k| self.labelled( k ) ).collect() )
.collect(); .collect();
let widest = rows.iter() let unit = key_h;
.map( |row| row.iter().map( |k| k.width ).sum::<f32>() )
.fold( 0.0_f32, f32::max )
.max( 1.0 );
let unit = ( avail - gap * ( rows.iter().map( Vec::len ).max().unwrap_or( 1 ).saturating_sub( 1 ) ) as f32 ) / widest;
for row in &rows for row in &rows
{ {
let units: f32 = row.iter().map( |k| k.width ).sum(); let units: f32 = row.iter().map( |k| k.width ).sum();
@@ -377,7 +385,7 @@ impl Panel
{ {
let key = self.keys[ idx ].rect; let key = self.keys[ idx ].rect;
let gap = GAP * self.scale; let gap = GAP * self.scale;
let w = key.width.max( KEY_HEIGHT * self.scale ); let w = key.width.max( key.height );
let total = variants.len() as f32 * w + ( variants.len().saturating_sub( 1 ) ) as f32 * gap; let total = variants.len() as f32 * w + ( variants.len().saturating_sub( 1 ) ) as f32 * gap;
let x0 = ( key.x + key.width / 2.0 - total / 2.0 ).max( self.rect.x ).min( self.rect.x + self.rect.width - total ); let x0 = ( key.x + key.width / 2.0 - total / 2.0 ).max( self.rect.x ).min( self.rect.x + self.rect.width - total );
let y = key.y - key.height - gap; let y = key.y - key.height - gap;
@@ -486,7 +494,7 @@ mod tests
fn panel() -> Panel fn panel() -> Panel
{ {
let mut p = Panel::new( Vec::new(), "en" ); let mut p = Panel::new( Vec::new(), "en" );
p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 1.0 ) }, 1.0 ); p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 400.0, 1.0 ) }, 1.0 );
p p
} }
@@ -591,10 +599,10 @@ mod tests
{ {
let mut p = panel(); let mut p = panel();
let now = Instant::now(); let now = Instant::now();
let plain = p.height( 1.0 ); let plain = p.height( 400.0, 1.0 );
p.set_purpose( Purpose::Terminal ); p.set_purpose( Purpose::Terminal );
assert!( p.height( 1.0 ) > plain ); assert!( p.height( 400.0, 1.0 ) > plain );
p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 1.0 ) }, 1.0 ); p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 400.0, 1.0 ) }, 1.0 );
let esc = p.keys().iter().find( |k| k.key.keysym == Some( Keysym::Escape ) ).unwrap().rect; let esc = p.keys().iter().find( |k| k.key.keysym == Some( Keysym::Escape ) ).unwrap().rect;
let at = Point { x: esc.x + 1.0, y: esc.y + 1.0 }; let at = Point { x: esc.x + 1.0, y: esc.y + 1.0 };
@@ -611,7 +619,7 @@ mod tests
assert!( !p.modifiers().ctrl ); assert!( !p.modifiers().ctrl );
p.set_purpose( Purpose::Normal ); p.set_purpose( Purpose::Normal );
assert_eq!( p.height( 1.0 ), plain ); assert_eq!( p.height( 400.0, 1.0 ), plain );
} }
#[ test ] #[ test ]
+37
View File
@@ -101,6 +101,24 @@ impl WallpaperBundle
( Arc::clone( &self.landscape.0 ), self.landscape.1, self.landscape.2 ) ( Arc::clone( &self.landscape.0 ), self.landscape.1, self.landscape.2 )
} }
/// A bundle `divisor` times smaller on each side and blurred by `sigma`
/// pixels of that size, for a backdrop the renderer scales back up: a
/// fraction of the memory, and the blur hides what the downscale lost.
pub fn blurred( &self, divisor: u32, sigma: f32 ) -> Self
{
let ( ref rgba, w, h ) = self.landscape;
let divisor = divisor.max( 1 );
let nw = ( w / divisor ).max( 1 );
let nh = ( h / divisor ).max( 1 );
let Some( view ) = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw( w, h, rgba.as_slice() ) else
{
return Self::from_decoded( self.landscape() );
};
let small = image::imageops::resize( &view, nw, nh, image::imageops::FilterType::Triangle );
let blurred = image::imageops::fast_blur( &small, sigma );
Self::from_decoded( ( Arc::new( blurred.into_raw() ), nw, nh ) )
}
/// Return the wallpaper variant appropriate for a surface of `(sw, sh)`. /// Return the wallpaper variant appropriate for a surface of `(sw, sh)`.
/// ///
/// - Landscape surface (`sw >= sh`): returns the full landscape image. /// - Landscape surface (`sw >= sh`): returns the full landscape image.
@@ -279,4 +297,23 @@ mod tests
let ( _, w, h ) = bundle.for_size( 0, 0 ); let ( _, w, h ) = bundle.for_size( 0, 0 );
assert_eq!( ( w, h ), ( 4, 4 ) ); assert_eq!( ( w, h ), ( 4, 4 ) );
} }
#[ test ]
fn blurred_is_divided_and_never_empty()
{
let bundle = WallpaperBundle::from_decoded( xgrad( 64, 32 ) );
let ( rgba, w, h ) = bundle.blurred( 4, 1.0 ).landscape();
assert_eq!( ( w, h ), ( 16, 8 ) );
assert_eq!( rgba.len(), 16 * 8 * 4 );
let ( _, w, h ) = WallpaperBundle::from_decoded( xgrad( 2, 2 ) ).blurred( 8, 1.0 ).landscape();
assert_eq!( ( w, h ), ( 1, 1 ) );
}
#[ test ]
fn blurred_keeps_a_flat_colour()
{
let flat = ( Arc::new( [ 10u8, 20, 30, 255 ].repeat( 8 * 8 ) ), 8, 8 );
let ( rgba, _, _ ) = WallpaperBundle::from_decoded( flat ).blurred( 2, 2.0 ).landscape();
assert!( rgba.chunks( 4 ).all( |px| px == [ 10, 20, 30, 255 ] ) );
}
} }