Hover & Pressed States
Define visual changes for different interaction states.
Every value inside a state layer is reactive, like the base property it covers.
when_hovered(|s| s.background(theme.accent)) follows the theme; it does not
take a snapshot of it when the widget is built.
Hover State
Applied when the mouse cursor is over the widget:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.background(Color::rgb(0.2, 0.2, 0.3))
.when_hovered(|s| s.lighter(0.1))
;
}
Common Hover Patterns
Lighten background:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_hovered(|s| s.lighter(0.1))
;
}
Explicit color change:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_hovered(|s| s.background(Color::rgb(0.4, 0.6, 0.9)))
;
}
Border highlight:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.border(1.0, Color::rgb(0.3, 0.3, 0.4))
.when_hovered(|s| s.border(1.0, Color::rgb(0.5, 0.5, 0.6)))
;
}
The width stays: a border takes room inside the box, so a hover that widened it would move the content by the difference.
Ring:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.outline(2.0, Color::TRANSPARENT, 2.0)
.when_hovered(|s| s.outline(2.0, Color::rgb(0.5, 0.5, 0.6), 2.0))
;
}
An outline is drawn outside the box and takes no room, so showing it moves nothing.
Shadow lift:
extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.shadow(LOW)
.when_hovered(|s| s.shadow(RAISED))
;
}
Pressed State
Applied when the mouse button is held down:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.background(Color::rgb(0.2, 0.2, 0.3))
.when_pressed(|s| s.darker(0.1))
;
}
Common Pressed Patterns
Darken background:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.darker(0.1))
;
}
Scale down (tactile feedback):
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.scale(0.98))
;
}
Shrink the shadow (press into the surface):
extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.shadow(RAISED)
.when_pressed(|s| s.shadow(LOW))
;
}
Ripple effect:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_pressed(|s| s.ripple())
;
}
Combining Hover and Pressed
Most interactive elements use both states:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.background(Color::rgb(0.2, 0.2, 0.3))
.when_hovered(|s| s.lighter(0.1))
.when_pressed(|s| s.ripple().darker(0.05))
;
}
Text Colour
A state layer can change the colour of the text below the container, not just the box around it:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
container()
.control() // the text below declares .when_hovered(|s| s.color(theme.text))
.child(text("Label").color(theme.text_weak))
;
}
This works the same way the ordinary declaration does — the container publishes its text colour to descendants, and here what it publishes folds in the hover. A text that declares its own colour nearer down is unaffected, and a container with no state layer mentioning text creates nothing extra.
The base it returns to when the state ends is the colour that would have been inherited anyway, so a container can override on hover without restating what its ancestors already said:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
container()
// set once, further up
.child(
container()
.control() // the text below declares .when_hovered(|s| s.color(theme.text))
.child(text("Label").color(theme.text_weak)), // weak, then strong, then weak
)
;
}
It cannot be animated. A container transitions background, border_color,
border_width, corners, shadow, width, height, padding,
translate, rotate and scale; text colour is not among them, so the
override lands in one frame.
A transition declared on two levels — an animated value whose own base comes from an ancestor that is itself animating — retargets every frame, which gives a damped chase rather than a transition with its own curve. It converges either way; CSS instead starts the inner one once, towards the outer’s final value.
States Your App Owns
Hover, pressed and focus are noticed by the container. The rest — the last submit failed, this row is selected, this value is out of range — are conditions your app already holds, so there is nothing to propagate: pass the signal and it is read where the style is resolved.
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
let wrong_password = create_signal(false);
container()
.border(1.0, theme.line)
.state(wrong_password, |s| s.border(1.0, theme.error))
.child(password_input(password))
;
}
Because it is just a signal, anything else that needs it reads the same one, independently — a label beside the field does not have to be told:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let theme = Theme::default();
let wrong_password = create_signal(false);
container()
.state(wrong_password, |s| s.border(1.0, theme.error))
.child(text("Wrong password"))
;
}
Whose Hover?
A label inside a button has to light up while the pointer is on the button’s padding, nowhere near the glyphs. And it has to stay dark while the pointer is over a different button in the same row. So neither “my own bounds” nor “any ancestor” is the answer.
The answer is the nearest enclosing control — the interaction unit the widget belongs to:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let save = move || {};
let theme = Theme::default();
container()
.padding(12.0)
.on_click(save)
.child(text("Save").color(theme.weak).when_hovered(|s| s.color(theme.strong)))
;
}
on_click makes that container a control, because anything the pointer can act
on is a unit by necessity. So do on_hover, on_scroll, scroll and any
declared state layer. Write control() yourself where the boundary is real but
nothing else announces it:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
container().control()
.child(text("Password").when_focused(|s| s.color(theme.accent)))
.child(password_input(password))
;
}
That is the case the old rule could not express at all: the focus is in a sibling, not below the label. Both belong to the same unit, so the label can ask about it.
Leaves get when_hovered, when_pressed, when_focused and state from the
Stateful trait, and the closure hands back the same builder the widget itself
uses.
Nesting scopes resolution, not state. A list row stays hovered while the pointer is over a button nested in it — the pointer is plainly still on the row. What the nested control changes is only who a descendant asks.
With nothing marked above it, a widget is its own unit and notices the pointer over its own bounds. It is never pressed: being pressed means being activated, and it has nothing to activate.
when_focused also keeps the focus. A press that no widget claimed takes
the keyboard off whatever held it, and a press inside a container declaring
when_focused while that focus is inside it is not one of those. A box that
says it lights up for a field has said the box is part of the field, so
clicking its padding does not blur what it draws.
It keeps the focus without consuming the press. A clickable row wrapping a
decorated field still sees the click, still lights its pressed layer and still
fires its on_click — keeping the keyboard and swallowing the event are two
different things. See
Losing Focus.
Which Layer Wins
Layers are resolved in reverse declaration order, one property at a time: the last one you wrote that is active and speaks about that property wins.
This is what lets a field say that its error outranks its focus ring — which matters on a password field, where the focus is held essentially all the time:
extern crate guido;
use guido::prelude::*;
#[derive(Clone, Copy)]
struct Theme { text: Color, text_weak: Color, weak: Color, strong: Color, line: Color, accent: Color, error: Color, danger: Color, surface: Color }
impl Default for Theme { fn default() -> Self { let c = Color::WHITE; Self { text: c, text_weak: c, weak: c, strong: c, line: c, accent: c, error: c, danger: c, surface: c } } }
fn main() {
let password = create_password();
let theme = Theme::default();
let wrong_password = create_signal(false);
container()
.border(1.0, theme.line)
.outline(2.0, Color::TRANSPARENT, 2.0)
.when_focused(|s| s.outline(2.0, theme.accent, 2.0))
.state(wrong_password, |s| s.outline(2.0, theme.error, 2.0)) // written after,
.child(password_input(password)) // so it wins
;
}
Swap the two lines and the focus ring wins instead. A layer that says nothing about a property is skipped rather than ending the search, so the pressed layer below only takes over the transform and leaves the hover’s background alone:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.when_hovered(|s| s.lighter(0.1))
.when_pressed(|s| s.scale(0.98))
;
}
Keep in mind that a layer replaces the base rather than ranking against it. A border that already carries a meaning does not belong in the base — put it in a layer with a condition, where the ordering above can speak about it.
Combining Multiple Overrides
Each state can override multiple properties:
extern crate guido;
use guido::prelude::*;
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.when_hovered(|s| s
.lighter(0.1)
.outline(2.0, Color::rgb(0.5, 0.7, 1.0), 2.0)
.shadow(RAISED)
)
.when_pressed(|s| s
.ripple()
.darker(0.05)
.scale(0.98)
.shadow(LOW)
)
;
}
With Animations
Add transitions for smooth state changes:
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.background(Color::rgb(0.3, 0.5, 0.8).transition(200.0))
.outline(2.0, Color::TRANSPARENT.transition(150.0), 2.0)
.scale(Scale::NONE.transition(Transition::spring(SpringConfig::GENTLE)))
.when_hovered(|s| s.lighter(0.1).outline(2.0, Color::WHITE, 2.0))
.when_pressed(|s| s.darker(0.1).scale(0.98))
;
}
Button Patterns
Simple Button
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.padding(12.0)
.background(Color::rgb(0.3, 0.5, 0.8))
.corners(6.0)
.when_hovered(|s| s.lighter(0.1))
.when_pressed(|s| s.ripple())
.on_click(|| println!("Clicked!"))
.child(container().child(text("Click me").color(Color::WHITE)))
;
}
Outlined Button
extern crate guido;
use guido::prelude::*;
fn main() {
container()
.padding(12.0)
.background(Color::TRANSPARENT)
.corners(6.0)
.border(1.0, Color::rgb(0.5, 0.5, 0.6))
.when_hovered(|s| s.background(Color::rgba(1.0, 1.0, 1.0, 0.1)))
.when_pressed(|s| s.ripple())
.child(container().child(text("Outlined").color(Color::WHITE)))
;
}
Card with Lift
// not compiled: `[...]` stands in for the children; this block is about
// the shadows above them.
extern crate guido;
use guido::prelude::*;
const FLAT: Shadow = Shadow::none();
const LOW: Shadow = Shadow::simple((0.0, 1.0), 3.0, Color::rgba(0.0, 0.0, 0.0, 0.12));
const RAISED: Shadow = Shadow::simple((0.0, 3.0), 6.0, Color::rgba(0.0, 0.0, 0.0, 0.19));
fn main() {
container()
.padding(16.0)
.background(Color::rgb(0.15, 0.15, 0.2))
.corners(8.0)
.shadow(LOW.transition(200.0))
.when_hovered(|s| s.shadow(RAISED).lighter(0.03))
.when_pressed(|s| s.shadow(FLAT))
.children([...])
;
}
API Reference
StateStyle Builder
// not compiled: a signature listing — these declarations have no bodies.
impl StateStyleBuilder {
// Background
pub fn background(self, color: Color) -> Self;
pub fn lighter(self, amount: f32) -> Self;
pub fn darker(self, amount: f32) -> Self;
pub fn background_alpha<M>(self, alpha: impl IntoSignal<f32, M>) -> Self;
// The whole container, children included — its own `opacity`
pub fn opacity<M>(self, opacity: impl IntoSignal<f32, M>) -> Self;
// Border — both halves, always; half a border is no border
pub fn border(self, width: f32, color: Color) -> Self;
// Outline — width, colour and offset, always; it takes no room
pub fn outline(self, width: f32, color: Color, offset: f32) -> Self;
// Other
pub fn corners<M>(self, corners: impl IntoSignal<Corners, M>) -> Self;
// Every value is reactive here too, like the container's own
pub fn translate<M>(self, translate: impl IntoSignal<Translate, M>) -> Self;
pub fn rotate<M>(self, degrees: impl IntoSignal<f32, M>) -> Self;
pub fn scale<M>(self, factor: impl IntoSignal<Scale, M>) -> Self;
pub fn shadow<M>(self, shadow: impl IntoSignal<Shadows, M>) -> Self;
// Ripple
pub fn ripple(self) -> Self;
pub fn ripple_with_color<M>(self, color: impl IntoSignal<Color, M>) -> Self;
}