Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Styling Overview

This page provides a complete reference for all styling options available in Guido.

Backgrounds

Solid Color

extern crate guido;
use guido::prelude::*;
fn main() {
container().background(Color::rgb(0.2, 0.2, 0.3))
;
}

Gradients

extern crate guido;
use guido::prelude::*;
fn main() {
// Horizontal (left to right)
container().gradient(LinearGradient::horizontal(Color::RED, Color::BLUE));

// Vertical (top to bottom)
container().gradient(LinearGradient::vertical(Color::RED, Color::BLUE));

// Diagonal
container().gradient(LinearGradient::diagonal(Color::RED, Color::BLUE))
;
}

The stops blend as colours mix, so a gradient from Color::TRANSPARENT to red is red fading out, not a red darkening through grey.

Corners

Basic Radius

extern crate guido;
use guido::prelude::*;
fn main() {
container().corners(8.0)  // 8px radius on all corners
;
}

Corner Curvature

Control corner shape using CSS K-values:

extern crate guido;
use guido::prelude::*;
fn main() {
container().corners(Corners::squircle(12.0));  // iOS-style (K=2)
container().corners(Corners::scoop(12.0));              // Circular (K=1, default)
container().corners(Corners::bevel(12.0));      // Diagonal (K=0)
container().corners(Corners::superellipse(12.0, 1.5));      // Concave (K=-1)
container().corners(12.0)  // Custom
;
}

Borders

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .border(2.0, Color::WHITE)  // Width and color
;
}

A border takes room inside the box, as CSS’s box-sizing: border-box does: the children are laid out inside the border and the padding, and a box sized to its content grows by both. A scroller’s viewport and extents are measured inside its border, so its content scrolls from the border’s inner edge to the same edge at the far end. The width is a layout property for that reason: changing it, from a signal, a transition or a state layer, relays the container out and moves what it holds. A ring that should not move anything is an outline.

A container that scrolls or hides its overflow cuts its children inside its border, as CSS cuts at the padding box: the border is never covered by what the container holds, its scrollbar sits inside the border too, and a press on the border does not reach a child under it. The container’s own background, border and shadow are not cut by that clip.

Outlines

A ring outside the box, drawn above everything it holds and taking no room — the focus or hover ring. See Outlines.

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .corners(8.0)
    .outline(2.0, Color::WHITE, 2.0)  // Width, colour, offset
;
}

Shadows

Four degrees of freedom: offset, blur, spread and colour. There is no elevation level — a design system’s ladder is a set of Shadow constants the application writes down. See Shadows.

extern crate guido;
use guido::prelude::*;
fn main() {
container().shadow(Shadow::simple((0.0, 2.0), 4.0, Color::rgba(0.0, 0.0, 0.0, 0.16)));
container().shadow(Shadow::simple((0.0, 6.0), 10.0, Color::rgba(0.0, 0.0, 0.0, 0.22)));
container().shadow(Shadow::new((14.0, 0.0), 8.0, 4.0, Color::rgba(0.9, 0.2, 0.3, 0.5)))
;
}

Opacity

opacity fades a container and everything inside it — its background, border and shadow, and its children’s text, images and frost — from 1.0, as declared, down to 0.0, invisible. Nested opacities multiply: a half inside a half is a quarter.

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.2, 0.2, 0.3))
    .opacity(0.5)
    .child(text("half as strong"))
;
}

The container fades as one picture, the way CSS’s opacity does: where a label sits on its own background, or a cover under a title, the overlap fades with the rest instead of showing through. To get that, the container is drawn into a texture of its own and laid down once — but only when two of the things it draws actually overlap. Where nothing overlaps, fading each thing on its own is the same picture, and that is what is drawn, for nothing.

opacity_mode chooses the other behaviour: every draw fades on its own, an overlap shows through darker, and nothing is drawn offscreen.

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .background(Color::rgb(0.2, 0.2, 0.3))
    .opacity(0.5)
    .opacity_mode(OpacityMode::Modulate)
    .child(text("the background shows through the letters"))
;
}

A container with a backdrop blur in it fades draw by draw, whatever overlaps: its frost keeps frosting what guido drew beneath it in the surface for the whole of the fade. Faded as one picture, the frost would see only what the container itself drew — CSS calls that a backdrop root — and snap back to the surface when the fade ended. A blur the compositor runs behind the surface is not affected either way.

A faded container still takes up its space and still takes input.

Blur

blur softens everything a container draws, its children included, by a radius in logical pixels — the radius a backdrop blur takes:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .blur(4.0)
    .child(text("out of focus"))
;
}

A blur, like a CSS filter, always draws the container into a texture of its own and blurs that. The texture covers what the container draws, its children included, plus the reach of the blur, cut to the part that can be on the surface. A radius of zero is drawn the same way and looks exactly like no blur. A backdrop blur inside a blurred container frosts only what the container drew beneath it. The blur spreads past everything the container draws — its edges, or a child’s shadow standing outside them — by about one and a quarter of its radius. It animates — see Animated Properties.

Padding

extern crate guido;
use guido::prelude::*;
fn main() {
container().padding(16.0);              // All sides
container().padding(16);                // Integers work too
container().padding([8.0, 16.0]);       // [vertical, horizontal]
container().padding([8, 16]);           // Integer arrays too
container().padding([1.0, 2.0, 3.0, 4.0]);  // [top, right, bottom, left]
container().padding(Padding::all(8.0).with_top(20.0))  // Builder for one edge
;
}

Sizing

Fixed Size

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .width(100.0)
    .height(50.0)
;
}

Integers work too:

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .width(100)
    .height(50)
;
}

Constraints

extern crate guido;
use guido::prelude::*;
fn main() {
container()
    .width(at_least(50.0).at_most(200.0))
    .height(at_least(30.0).at_most(100.0))
;
}

At Least / At Most

extern crate guido;
use guido::prelude::*;
fn main() {
container().width(at_least(100.0));           // At least 100px
container().width(at_least(100));             // Integers work too
container().width(at_most(400));              // At most 400px
container().width(at_least(100).at_most(400)) // Range
;
}

Fraction of Available Space

fraction(f) takes a fraction (0.0..=1.0) of the space offered by the parent, resolved at layout time. It is the natural tool for value-proportional bars — sliders, gauges, progress — because the width follows the value on the very first frame, with no measured-rect round-trip:

extern crate guido;
use guido::prelude::*;
fn main() {
// A slider fill bar at the current volume
let volume = create_signal(40);
container().width(move || fraction(volume.get() as f32 / 100.0))
;
}

Natural Size

natural() is the size of the content, whatever the parent offers: it is never cut to the parent, and runs past its edge unless the parent clips — see Natural Size in the Layout chapter.

extern crate guido;
use guido::prelude::*;
fn main() {
container().width(natural()).child(text("Never wrapped, never cut"))
;
}

Static or Reactive

Every property that survives to paint takes a signal, a closure, or a plain value — the same IntoSignal shape everywhere, so which spelling you use is never the API’s decision:

extern crate guido;
use guido::prelude::*;
const COOL: Color = Color::rgb(0.2, 0.4, 0.9);
const HOT: Color = Color::rgb(0.9, 0.3, 0.2);
#[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 collapsed = create_signal(false);
let frosted = create_signal(false);
let hot = create_signal(false);
#[derive(Clone, PartialEq)]
struct Palette;
impl Palette {
    fn header(&self) -> LinearGradient {
        LinearGradient::horizontal(Color::WHITE, Color::BLACK)
    }
}
let palette = create_signal(Palette);
let surface_color = create_signal(Color::rgb(0.1, 0.1, 0.15));
let theme = Theme::default();
container()
    .background(theme.surface)                       // a constant
    .background(surface_color)                       // a signal
    .background(move || if hot.get() { HOT } else { COOL })   // a closure
    .gradient(move || palette.get().header())
    .backdrop_blur(move || if frosted.get() { 24.0 } else { 0.0 })
    .overflow(move || if collapsed.get() { Overflow::Hidden } else { Overflow::Visible })
;
}

A blur radius of 0.0 is “no blur”, on a container and on a text alike, which is what lets one signal switch the effect on and off rather than forcing the caller to rebuild the widget in a Rust branch. That holds wherever the surface is one of the sources, which is everything by default. A container that restricts itself to the compositor has no radius of its own — the protocol carries none — so it asks on its sources alone, and empties them to switch off.

What is not reactive is structural: .layout(..), the axis a .scroll(..) is built with, .control(), and the motion a value is declared with — .transition(..) and .timeline(..). These say what kind of thing the container is; change one and you are describing a different widget, so declare it in the closure that builds the widget instead. The value a motion decorates is as reactive as any other.

Complete Example

extern crate guido;
use guido::prelude::*;
fn styled_card(title: &str, content: &str) -> Container {
    container()
        // Size and padding
        .width(300.0)
        .padding(20.0)

        // Background and corners — the background eases, so the hover below
        // arrives over 200ms rather than in one frame
        .background(Color::rgb(0.15, 0.15, 0.2).transition(200.0))
        .corners(Corners::squircle(12.0))
        

        // Border
        .border(1.0, Color::rgb(0.25, 0.25, 0.3))

        // Shadow
        .shadow(Shadow::simple((0.0, 4.0), 8.0, Color::rgba(0.0, 0.0, 0.0, 0.2)))

        // Layout
        .layout(Flex::column().spacing(12.0))

        // State layers — they supply values; the motion was declared above
        .when_hovered(|s| {
            s.lighter(0.05)
                .shadow(Shadow::simple((0.0, 6.0), 12.0, Color::rgba(0.0, 0.0, 0.0, 0.24)))
        })

        // Children
        .children([
            container().child(text(title).font_size(18.0).color(Color::WHITE)),
            container().child(text(content).font_size(14.0).color(Color::rgb(0.7, 0.7, 0.75))),
        ])
}
fn main() {}