Slint Kit Documentation

Colors

Color Picker for Slint

KitColorPicker · Slint Kit 0.3.0

Color editor with hex text, channel inputs and channel sliders.

Color editor with hex text, channel inputs and channel sliders.
Native app built from the example below.

Try the component

Download the complete project, extract it and run cargo run --locked in its folder. Rust 1.92+; Slint Kit 0.3.0 from crates.io. You can also copy the four files below.

Edit a hexadecimal sRGB color or use the picker. Rust validates byte channels, keeps drafts separate and publishes the accepted color.

ui/app.slint

export { KitFormValidation, HtmlInputKind, NumberInputFormat, KitInputConstraints, KitValidationResult, KitValidityState } from "@kit/kit.slint";
import {KitColorPicker,KitThemes,SrgbColorView,ColorChannel,NavigationStep} from "@kit/kit.slint";
export {SrgbColorView,ColorChannel,NavigationStep}
export component App inherits Window {
 title:"color-picker";preferred-width:540px;preferred-height:800px;max-width:100000px;max-height:100000px;background:KitThemes.light.canvas;
 in-out property <string> hex;in property <string> error;in property <string> accepted;in property <SrgbColorView> color-value;in property <color> swatch;
 callback commit(string);callback edit();callback choose(SrgbColorView);callback cancel();in-out property <string> red;in-out property <string> green;in-out property <string> blue;in-out property <string> alpha;
 callback channel(ColorChannel,string);callback position(ColorChannel,float);callback navigate(ColorChannel,NavigationStep);callback begin-gesture(ColorChannel);callback finish(ColorChannel);callback cancel-channel(ColorChannel);
 VerticalLayout{padding:24px;spacing:16px;alignment:start;
 control:=KitColorPicker{label:"Accent color";accepted-color:root.color-value;swatch:root.swatch;color-requested(value)=>{root.choose(value);}hex-text<=>root.hex;red-text<=>root.red;green-text<=>root.green;blue-text<=>root.blue;alpha-text<=>root.alpha;hex-error:root.error;
 hex-edited(value)=>{root.edit();}hex-commit-requested(value)=>{root.commit(value);}
 channel-edited(channel,value)=>{root.edit();}channel-commit-requested(channel,value)=>{root.channel(channel,value);}
 channel-position-requested(channel,position)=>{root.position(channel,position);}channel-navigate(channel,step)=>{root.navigate(channel,step);}
 gesture-started(channel)=>{root.begin-gesture(channel);}committed(channel)=>{root.finish(channel);}cancelled(channel)=>{root.cancel-channel(channel);}}
 Text{text:"Accepted: "+root.accepted;color:KitThemes.light.text-primary;wrap:word-wrap;}
 }
}

src/main.rs

use slint::ComponentHandle;
use slint_kit::{AlphaPolicy, DraftValue, SrgbColor};
use std::{cell::RefCell, rc::Rc};
mod ui {
    slint::include_modules!();
}
fn publish(app: &ui::App, color: SrgbColor, accepted: bool) {
    app.set_hex(color.hex(AlphaPolicy::Opaque).into());
    app.set_color_value(ui::SrgbColorView {
        red: color.red.into(),
        green: color.green.into(),
        blue: color.blue.into(),
        alpha: 255,
    });
    app.set_swatch(slint::Color::from_rgb_u8(
        color.red,
        color.green,
        color.blue,
    ));
    if accepted {
        app.set_accepted(color.hex(AlphaPolicy::Opaque).into());
    }
    app.set_red(color.red.to_string().into());
    app.set_green(color.green.to_string().into());
    app.set_blue(color.blue.to_string().into());
    app.set_alpha("255".into());
}
fn main() -> Result<(), slint::PlatformError> {
    let app = ui::App::new()?;
    slint_kit::init_form_validation!(ui, app);
    let initial = SrgbColor {
        red: 79,
        green: 70,
        blue: 229,
        alpha: 255,
    };
    let state = Rc::new(RefCell::new((initial, initial)));
    publish(&app, initial, true);
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_commit(move |text| {
        if let Some(app) = weak.upgrade() {
            match SrgbColor::parse_hex(&text, AlphaPolicy::Opaque) {
                DraftValue::Parsed(value) => {
                    *shared.borrow_mut() = (value, value);
                    publish(&app, value, true);
                    app.set_error("".into());
                }
                _ => app.set_error("Use #RRGGBB or #RGB hexadecimal".into()),
            }
        }
    });
    let weak = app.as_weak();
    app.on_edit(move || {
        if let Some(app) = weak.upgrade() {
            app.set_error("".into());
        }
    });
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_choose(move |view| {
        if let Some(app) = weak.upgrade() {
            if let (Ok(red), Ok(green), Ok(blue)) = (
                view.red.try_into(),
                view.green.try_into(),
                view.blue.try_into(),
            ) {
                let color = SrgbColor {
                    red,
                    green,
                    blue,
                    alpha: 255,
                };
                *shared.borrow_mut() = (color, color);
                publish(&app, color, true);
                app.set_error("".into());
            }
        }
    });

    fn set(color: &mut SrgbColor, channel: ui::ColorChannel, value: u8) {
        match channel {
            ui::ColorChannel::Red => color.red = value,
            ui::ColorChannel::Green => color.green = value,
            ui::ColorChannel::Blue => color.blue = value,
            ui::ColorChannel::Alpha => {}
        }
    }
    fn get(color: SrgbColor, channel: ui::ColorChannel) -> u8 {
        match channel {
            ui::ColorChannel::Red => color.red,
            ui::ColorChannel::Green => color.green,
            ui::ColorChannel::Blue => color.blue,
            ui::ColorChannel::Alpha => 255,
        }
    }
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_channel(move |_channel, _text| {
        if let Some(app) = weak.upgrade() {
            let channels = app
                .get_red()
                .parse::<u8>()
                .ok()
                .zip(app.get_green().parse::<u8>().ok())
                .zip(app.get_blue().parse::<u8>().ok());
            if let Some(((red, green), blue)) = channels {
                let color = SrgbColor {
                    red,
                    green,
                    blue,
                    alpha: 255,
                };
                *shared.borrow_mut() = (color, color);
                publish(&app, color, true);
                app.set_error("".into());
            } else {
                app.set_error("Use whole sRGB channels from 0 to 255".into());
            }
        }
    });
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_position(move |channel, position| {
        if let Some(app) = weak.upgrade() {
            let mut state = shared.borrow_mut();
            set(
                &mut state.1,
                channel,
                (position.clamp(0.0, 1.0) * 255.0).round() as u8,
            );
            publish(&app, state.1, false);
        }
    });
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_navigate(move |channel, step| {
        if let Some(app) = weak.upgrade() {
            let mut state = shared.borrow_mut();
            let old = get(state.1, channel) as i32;
            let next = match step {
                ui::NavigationStep::First => 0,
                ui::NavigationStep::Last => 255,
                ui::NavigationStep::Next => old + 1,
                ui::NavigationStep::Previous => old - 1,
                ui::NavigationStep::PageNext => old + 16,
                ui::NavigationStep::PagePrevious => old - 16,
            };
            set(&mut state.1, channel, next.clamp(0, 255) as u8);
            state.0 = state.1;
            publish(&app, state.0, true);
        }
    });
    let shared = state.clone();
    app.on_begin_gesture(move |_| {
        let mut state = shared.borrow_mut();
        state.1 = state.0;
    });
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_finish(move |_| {
        if let Some(app) = weak.upgrade() {
            let mut state = shared.borrow_mut();
            state.0 = state.1;
            publish(&app, state.0, true);
        }
    });
    let weak = app.as_weak();
    let shared = state.clone();
    app.on_cancel_channel(move |_| {
        if let Some(app) = weak.upgrade() {
            let mut state = shared.borrow_mut();
            state.1 = state.0;
            publish(&app, state.0, true);
            app.set_error("".into());
        }
    });

    app.run()
}

Cargo.toml and build.rs

Cargo.toml

[package]
name = "slint-kit-color-picker-example"
version = "0.1.0"
edition = "2024"
rust-version = "1.92"
license = "GPL-3.0-or-later"
publish = false

[dependencies]
slint-kit = "=0.3.0"
slint = { version = "=1.18.1", default-features = false, features = ["std", "compat-1-18", "backend-winit", "renderer-skia", "accessibility"] }

[build-dependencies]
slint-kit-build = "=0.3.0"

[workspace]

build.rs

fn main() {
    slint_kit_build::compile("ui/app.slint").expect("compile component example");
}

Files: Cargo.toml · build.rs · ui/app.slint · src/main.rs

Keep accepted SrgbColor in Rust. Connect channel text commits, position requests and gesture start/commit/cancel callbacks.

The example accepts opaque hex colors and channel values from 0 to 255. Cancel restores the color saved at gesture start.

Choose between built-in options and custom composition

Shared options

Visual composition

The file below produces the screenshot. For controller-driven components, it shows the displayed state without supplying the Rust wiring. Create a project.

Show the screenshot source
Color editor with hex text, channel inputs and channel sliders.
Native screenshot of the same example, rendered with Skia.
// SPDX-License-Identifier: GPL-3.0-or-later
import { KitColorPicker, KitTheme, KitThemes } from "@kit/kit.slint";

export component Example inherits Window {
    title: "KitColorPicker example";
    preferred-width: 480px;
    preferred-height: 550px;
    max-width: 100000px;
    max-height: 100000px;
    background: root.theme.canvas;
    in-out property <KitTheme> theme: KitThemes.light;
    in-out property <string> status;
    VerticalLayout {
        padding: 24px; spacing: 16px; alignment: start;
        Text { text: "Color Picker"; color: root.theme.text-primary; font-size: 20px * root.theme.text-scale; font-weight: 600; }
        KitColorPicker { theme: root.theme; label: "Accent color"; swatch: #6366f1; accepted-color: { red: 99, green: 102, blue: 241, alpha: 255 }; hex-text: "#6366F1"; red-text: "99"; green-text: "102"; blue-text: "241"; }
        if root.status != "": Text { text: root.status; color: root.theme.text-secondary; font-size: 12px * root.theme.text-scale; wrap: word-wrap; }
    }
}

Download this example

Public API

Inherits Rectangle.

Properties

Name Type Direction
theme KitTheme in
control-size ControlSize in
access FieldAccess in
alpha-mode ColorAlphaMode in
accepted-color SrgbColorView in
swatch color in
label string in
hex-error string in
red-error string in
green-error string in
blue-error string in
alpha-error string in
hex-text string in-out
red-text string in-out
green-text string in-out
blue-text string in-out
alpha-text string in-out

Types and variants

Type Declaration
enum ColorAlphaMode opaque, editable
enum ColorChannel red, green, blue, alpha
enum ControlSize compact, medium, comfortable
enum FieldAccess editable, read-only, disabled
enum NavigationStep next, previous, first, last, page-next, page-previous
struct SrgbColorView red: int, green: int, blue: int, alpha: int

Callbacks

See also