Skip to content

theme

一番上で渡した 2 つの値を、3 階層下で読む。間のコンポーネントは何もしない。

Themed が Theme と Locale を持ち、どちらも子に配ります。間にいる Pageと Card は props を 1 つも取りません。テーマやロケールの存在すら知りません。3 階層下の葉が use_context でそれを読み、プロバイダ自身の子である Toggles が、同じ Handle を通して書き戻します。いちばん下のOrphan は Themed の外で描かれるので、その use_context は None を返します。束縛が効くのは、それを配った子が走っている間だけです。

プロバイダの形に注目してください。あなたが写すことになるのはこの形です。provide_context は子をクロージャとして取り、公開する Handle はストアを借りています。だからハンドルは、それを配るのと同じコンポーネントの本体の中で作らなければいけません。<Provide value={handle}> のような要素はありませんし、作れません。props の型はストアのライフタイムを名指しできないからです。プロバイダはまた #[component(shares_ui)] です。自分では何も描かないので、その子は自前の木ではなく、親のレイアウト木のノードになるべきだからです。

値の鍵は型です。だから Theme と Locale は 1 つの構造体の 2 フィールドではなく、別々の型になっています。use_context::<Theme> はこれだけを見つけます。そして use_state ではなく use_handle です。Handle は Copy で借用を持たないので、プロバイダが走っている最中でも、部分木がそれを通して書き戻せます。

自分で動かす ​

sh
cargo run -p theme
trunk serve --config examples/theme/Trunk.toml

ソースは examples/theme/src/lib.rs です。

rust
use egui_reactor::prelude::*;
use egui_reactor_elements::prelude::*;

/// One of the two provided values. A type of its own, because the context is
/// keyed by type: `use_context::<Theme>` finds this and nothing else.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Theme {
    pub dark: bool,
}

impl Theme {
    pub fn name(self) -> &'static str {
        if self.dark { "dark" } else { "light" }
    }

    /// The swatch colour, so a leaf has something to show for the theme
    /// besides its name.
    pub fn swatch(self) -> egui::Color32 {
        if self.dark {
            egui::Color32::from_rgb(0x2f, 0x45, 0x6e)
        } else {
            egui::Color32::from_rgb(0xcf, 0xdd, 0xf5)
        }
    }
}

/// The other provided value.
///
/// English and French rather than the Japanese the plan asked for: egui's
/// bundled fonts are Hack, Ubuntu-Light and two emoji faces, none of which has
/// CJK glyphs, so Japanese would draw as empty boxes. Loading a font belongs in
/// an example about fonts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Locale {
    En,
    Fr,
}

impl Locale {
    pub fn other(self) -> Self {
        match self {
            Self::En => Self::Fr,
            Self::Fr => Self::En,
        }
    }

    pub fn greeting(self) -> &'static str {
        match self {
            Self::En => "Hello",
            Self::Fr => "Bonjour",
        }
    }

    pub fn press(self) -> &'static str {
        match self {
            Self::En => "press me",
            Self::Fr => "appuyez ici",
        }
    }

    /// The label of the button that switches to the other language.
    pub fn switch(self) -> &'static str {
        match self {
            Self::En => "français",
            Self::Fr => "English",
        }
    }
}

#[component]
pub fn App(cx: &mut Cx) {
    rsx! {
        <View direction="column" gap={8} p={12} grow={1.0}>
            <Text size={22.0} strong>"theme"</Text>

            <Themed>
                <Toggles/>
                <Separator/>
                <Page/>
            </Themed>

            <Separator/>

            // Outside the provider.
            <Orphan/>
        </View>
    }
}

/// Owns both values and publishes them to its children.
///
/// `use_handle`, not `use_state`: a `Handle` is `Copy` and holds no borrow, so
/// it can be published to a subtree that then writes back through it. A guard
/// could do neither.
///
/// `shares_ui`, like `<Suspense>`: the provider draws nothing of its own, so
/// its children should become nodes of the parent's taffy tree rather than of
/// a tree of their own.
#[component(shares_ui)]
fn Themed(cx: &mut Cx, children: impl View) {
    let theme = use_handle(cx, || Theme { dark: true });
    let locale = use_handle(cx, || Locale::En);

    // Follow the theme with egui's own visuals. Inside the gallery this
    // restyles the gallery too, because `set_visuals` is per `egui::Context`
    // and there is one of those, not one per example. That is what the egui
    // API does and the example does not hide it.
    let dark = theme.get().dark;
    let ctx = cx.ctx().clone();
    use_effect(cx, dark, move || {
        ctx.set_visuals(if dark {
            egui::Visuals::dark()
        } else {
            egui::Visuals::light()
        });
    });

    provide_context(cx, theme, |cx| {
        provide_context(cx, locale, |cx| children.show(cx));
    });
}

/// A child of the provider that writes back through it, the way a React
/// `useTheme()` hook hands out both the value and its setter.
#[component]
fn Toggles(cx: &mut Cx) {
    let Some(theme) = use_context::<Theme>(cx) else {
        return;
    };
    let Some(locale) = use_context::<Locale>(cx) else {
        return;
    };
    let dark = theme.get().dark;

    rsx! {
        <View direction="row" gap={8} align="center">
            <Button on_click={move || theme.set(Theme { dark: !dark })}>
                {if dark { "switch to light" } else { "switch to dark" }}
            </Button>
            <Button on_click={move || locale.set(locale.get().other())}>
                {locale.get().switch()}
            </Button>
        </View>
    }
}

/// Takes no props. It does not know a theme or a locale exists.
#[component]
fn Page(cx: &mut Cx) {
    rsx! {
        <View direction="column" gap={8}>
            <Text>"Page: passes nothing down."</Text>
            <Card/>
        </View>
    }
}

/// Also takes no props.
#[component]
fn Card(cx: &mut Cx) {
    rsx! {
        <View p={12}>
            <View direction="column" gap={8}>
                <Greeting/>
                <ThemedButton/>
                <Swatch/>
            </View>
        </View>
    }
}

/// Three levels below `Themed`, and the first thing here that reads a context.
#[component]
fn Greeting(cx: &mut Cx) {
    let locale = use_context::<Locale>(cx);
    let text = locale.map_or("(no locale)", |locale| locale.get().greeting());
    rsx! { <Text size={18.0}>{text}</Text> }
}

/// A localised button with a count of its own: a leaf can hold state as well as
/// read a context.
#[component]
fn ThemedButton(cx: &mut Cx) {
    let locale = use_context::<Locale>(cx);
    let mut clicks = use_state(cx, || 0u32);
    let label = locale.map_or("(no locale)", |locale| locale.get().press());

    rsx! {
        <View direction="row" gap={8} align="center">
            <Button on_click={|| *clicks += 1}>{label}</Button>
            <Text>{format!("pressed {} times", *clicks)}</Text>
        </View>
    }
}

/// Reads the other context. The mode is in a label so a test can read it back.
#[component]
fn Swatch(cx: &mut Cx) {
    let theme = use_context::<Theme>(cx).map_or(Theme { dark: true }, |theme| theme.get());

    rsx! {
        <View direction="row" gap={8} align="center">
            <View bg={theme.swatch()} p={8} radius={4.0}>
                <Text>"swatch"</Text>
            </View>
            <Text>{format!("mode: {}", theme.name())}</Text>
        </View>
    }
}

/// Drawn outside the provider, so the lookup misses.
#[component]
fn Orphan(cx: &mut Cx) {
    let found = use_context::<Theme>(cx).is_some();
    rsx! {
        <Text>{if found { "outside: theme found" } else { "outside: no theme provided" }}</Text>
    }
}

MIT または Apache-2.0 ライセンスで公開しています。