mirror of
https://github.com/emilk/egui.git
synced 2026-09-03 07:10:04 -04:00
Better cache, StyleArgs and deadlock avoidance
This commit is contained in:
@@ -38,7 +38,7 @@ use crate::{
|
|||||||
resize, response, scroll_area, theme_plugin,
|
resize, response, scroll_area, theme_plugin,
|
||||||
util::IdTypeMap,
|
util::IdTypeMap,
|
||||||
viewport::ViewportClass,
|
viewport::ViewportClass,
|
||||||
widget_style::{Classes, WidgetState, WidgetStyle},
|
widget_style::{StyleArgs, WidgetStyle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::IdMap;
|
use crate::IdMap;
|
||||||
@@ -2039,7 +2039,7 @@ impl Context {
|
|||||||
/// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
|
/// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
|
||||||
pub fn add_widget_theme<S: WidgetStyle + 'static>(
|
pub fn add_widget_theme<S: WidgetStyle + 'static>(
|
||||||
&self,
|
&self,
|
||||||
theme: impl theme_plugin::ThemeStyle<S> + Send + Sync + 'static,
|
theme: impl theme_plugin::StyleProvider<S> + Send + Sync + 'static,
|
||||||
) {
|
) {
|
||||||
self.write(|ctx| ctx.themes.register::<S>(theme, false));
|
self.write(|ctx| ctx.themes.register::<S>(theme, false));
|
||||||
}
|
}
|
||||||
@@ -2050,19 +2050,18 @@ impl Context {
|
|||||||
/// This allow to live edit a theme.
|
/// This allow to live edit a theme.
|
||||||
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
|
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
|
||||||
&self,
|
&self,
|
||||||
theme: impl theme_plugin::ThemeStyle<S> + Send + Sync + 'static,
|
theme: impl theme_plugin::StyleProvider<S> + Send + Sync + 'static,
|
||||||
) {
|
) {
|
||||||
self.write(|ctx| ctx.themes.register::<S>(theme, true));
|
self.write(|ctx| ctx.themes.register::<S>(theme, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the [`WidgetStyle`] using the registered theme.
|
/// Compute the [`WidgetStyle`] using the registered theme.
|
||||||
pub(crate) fn get_widget_style<S: WidgetStyle + Clone + 'static>(
|
pub fn get_widget_style<S: WidgetStyle + Clone + 'static>(
|
||||||
&self,
|
&self,
|
||||||
ui: &Ui,
|
modifiers: &StyleArgs<'_>,
|
||||||
classes: &Classes,
|
|
||||||
state: WidgetState,
|
|
||||||
) -> S {
|
) -> S {
|
||||||
self.read(move |ctx| ctx.themes.get::<S>(ui, classes, state))
|
let theme = self.read(move |ctx| ctx.themes.get::<S>());
|
||||||
|
theme.lock().style(modifiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,33 +8,39 @@ use crate::{
|
|||||||
util::IdTypeMap,
|
util::IdTypeMap,
|
||||||
widget_style::{
|
widget_style::{
|
||||||
BaseStyle, ButtonStyle, CheckboxStyle, Classes, HasClasses as _, LabelStyle,
|
BaseStyle, ButtonStyle, CheckboxStyle, Classes, HasClasses as _, LabelStyle,
|
||||||
SELECTED_CLASS, SeparatorStyle, TextVisuals, WidgetState, WidgetStyle,
|
SELECTED_CLASS, SeparatorStyle, StyleArgs, TextVisuals, WidgetState, WidgetStyle,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A cache that can be implemented to reduce computation time of a `ThemeStyle`
|
/// A cache that can be implemented to reduce computation time of a `ThemeStyle`
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ThemeCache {
|
pub struct ThemeCache<Theme> {
|
||||||
cache: IdTypeMap,
|
cache: IdTypeMap,
|
||||||
|
inner: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeCache {
|
impl<Theme> ThemeCache<Theme> {
|
||||||
|
pub fn new(theme: Theme) -> Self {
|
||||||
|
Self {
|
||||||
|
cache: IdTypeMap::default(),
|
||||||
|
inner: theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Theme: StyleProvider<S>, S: WidgetStyle> StyleProvider<S> for ThemeCache<Theme> {
|
||||||
/// Access the cache for the requested [`WidgetStyle`] based on the [`Classes`] and
|
/// Access the cache for the requested [`WidgetStyle`] based on the [`Classes`] and
|
||||||
/// the [`WidgetState`]
|
/// the [`WidgetState`]
|
||||||
///
|
///
|
||||||
/// If no entry match the parameter then compute the fallback style and
|
/// If no entry match the parameter then compute the fallback style and
|
||||||
/// save the output for later.
|
/// save the output for later.
|
||||||
pub fn get<S: WidgetStyle + 'static>(
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> S {
|
||||||
&mut self,
|
let StyleArgs { classes, state, .. } = modifiers;
|
||||||
classes: &Classes,
|
|
||||||
state: WidgetState,
|
|
||||||
fallback: impl FnOnce() -> S,
|
|
||||||
) -> S {
|
|
||||||
let style_id = Id::new(classes).with(state);
|
let style_id = Id::new(classes).with(state);
|
||||||
if let Some(style) = self.cache.get_temp::<S>(style_id) {
|
if let Some(style) = self.cache.get_temp::<S>(style_id) {
|
||||||
style
|
style
|
||||||
} else {
|
} else {
|
||||||
let style = fallback();
|
let style = self.inner.style(modifiers);
|
||||||
self.cache.insert_temp(style_id, style.clone());
|
self.cache.insert_temp(style_id, style.clone());
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
@@ -42,9 +48,9 @@ impl ThemeCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
|
/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
|
||||||
pub trait ThemeStyle<S> {
|
pub trait StyleProvider<S> {
|
||||||
/// The style according to the classes and state of the widget
|
/// The style according to the classes and state of the widget
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> S;
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> S;
|
||||||
|
|
||||||
/// Help to differ the different themes
|
/// Help to differ the different themes
|
||||||
fn theme_type_id(&self) -> TypeId
|
fn theme_type_id(&self) -> TypeId
|
||||||
@@ -58,9 +64,9 @@ pub trait ThemeStyle<S> {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct DefaultStyle;
|
struct DefaultStyle;
|
||||||
|
|
||||||
impl ThemeStyle<BaseStyle> for DefaultStyle {
|
impl StyleProvider<BaseStyle> for DefaultStyle {
|
||||||
fn style(&mut self, ui: &Ui, _classes: &Classes, state: WidgetState) -> BaseStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> BaseStyle {
|
||||||
let style = ui.style();
|
let StyleArgs { style, state, .. } = modifiers;
|
||||||
let spacing = &style.spacing;
|
let spacing = &style.spacing;
|
||||||
let widget_visuals = match state {
|
let widget_visuals = match state {
|
||||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
@@ -80,7 +86,8 @@ impl ThemeStyle<BaseStyle> for DefaultStyle {
|
|||||||
stroke: widget_visuals.fg_stroke,
|
stroke: widget_visuals.fg_stroke,
|
||||||
text: TextVisuals {
|
text: TextVisuals {
|
||||||
color: widget_visuals.text_color(),
|
color: widget_visuals.text_color(),
|
||||||
font_id: style
|
font_id: modifiers
|
||||||
|
.style
|
||||||
.override_font_id
|
.override_font_id
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| TextStyle::Body.resolve(style)),
|
.unwrap_or_else(|| TextStyle::Body.resolve(style)),
|
||||||
@@ -91,9 +98,15 @@ impl ThemeStyle<BaseStyle> for DefaultStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeStyle<ButtonStyle> for DefaultStyle {
|
impl StyleProvider<ButtonStyle> for DefaultStyle {
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> ButtonStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
|
||||||
let style = ui.style();
|
let StyleArgs {
|
||||||
|
ctx,
|
||||||
|
classes,
|
||||||
|
style,
|
||||||
|
state,
|
||||||
|
..
|
||||||
|
} = modifiers;
|
||||||
let spacing = &style.spacing;
|
let spacing = &style.spacing;
|
||||||
let mut widget_visuals = match state {
|
let mut widget_visuals = match state {
|
||||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
@@ -102,7 +115,7 @@ impl ThemeStyle<ButtonStyle> for DefaultStyle {
|
|||||||
WidgetState::Active => style.visuals.widgets.active,
|
WidgetState::Active => style.visuals.widgets.active,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut ws: BaseStyle = ui.get_widget_style(classes, state);
|
let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
if classes.has(SELECTED_CLASS) {
|
if classes.has(SELECTED_CLASS) {
|
||||||
let visuals = &style.visuals;
|
let visuals = &style.visuals;
|
||||||
@@ -128,9 +141,11 @@ impl ThemeStyle<ButtonStyle> for DefaultStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeStyle<CheckboxStyle> for DefaultStyle {
|
impl StyleProvider<CheckboxStyle> for DefaultStyle {
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> CheckboxStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
|
||||||
let style = ui.style();
|
let StyleArgs {
|
||||||
|
ctx, style, state, ..
|
||||||
|
} = modifiers;
|
||||||
let spacing = &style.spacing;
|
let spacing = &style.spacing;
|
||||||
let widget_visuals = match state {
|
let widget_visuals = match state {
|
||||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
@@ -139,7 +154,7 @@ impl ThemeStyle<CheckboxStyle> for DefaultStyle {
|
|||||||
WidgetState::Active => style.visuals.widgets.active,
|
WidgetState::Active => style.visuals.widgets.active,
|
||||||
};
|
};
|
||||||
|
|
||||||
let ws: BaseStyle = ui.get_widget_style(classes, state);
|
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
CheckboxStyle {
|
CheckboxStyle {
|
||||||
frame: Frame::new(),
|
frame: Frame::new(),
|
||||||
@@ -157,9 +172,10 @@ impl ThemeStyle<CheckboxStyle> for DefaultStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeStyle<LabelStyle> for DefaultStyle {
|
impl StyleProvider<LabelStyle> for DefaultStyle {
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> LabelStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> LabelStyle {
|
||||||
let ws: BaseStyle = ui.get_widget_style(classes, state);
|
let StyleArgs { ctx, .. } = modifiers;
|
||||||
|
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
LabelStyle {
|
LabelStyle {
|
||||||
frame: Frame {
|
frame: Frame {
|
||||||
@@ -176,9 +192,10 @@ impl ThemeStyle<LabelStyle> for DefaultStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeStyle<SeparatorStyle> for DefaultStyle {
|
impl StyleProvider<SeparatorStyle> for DefaultStyle {
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> SeparatorStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> SeparatorStyle {
|
||||||
let ws: BaseStyle = ui.get_widget_style(classes, state);
|
let StyleArgs { ctx, .. } = modifiers;
|
||||||
|
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
SeparatorStyle {
|
SeparatorStyle {
|
||||||
spacing: 6.0,
|
spacing: 6.0,
|
||||||
@@ -202,16 +219,13 @@ impl Ui {
|
|||||||
.map(|r| r.widget_state())
|
.map(|r| r.widget_state())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
self.get_widget_style::<S>(classes, state)
|
self.get_widget_style::<S>(&StyleArgs {
|
||||||
}
|
classes,
|
||||||
|
state,
|
||||||
/// Compute the [`WidgetStyle`] using the registered theme.
|
style: self.style(),
|
||||||
pub fn get_widget_style<S: WidgetStyle + Clone + 'static>(
|
stack: self.stack(),
|
||||||
&self,
|
ctx: self,
|
||||||
classes: &Classes,
|
})
|
||||||
state: WidgetState,
|
|
||||||
) -> S {
|
|
||||||
self.ctx().get_widget_style(self, classes, state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +233,7 @@ pub struct Themes {
|
|||||||
themes: IdTypeMap,
|
themes: IdTypeMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeWrap<S> = Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>;
|
type ThemeWrap<S> = Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>;
|
||||||
|
|
||||||
impl Default for Themes {
|
impl Default for Themes {
|
||||||
/// Register the default egui theme
|
/// Register the default egui theme
|
||||||
@@ -261,20 +275,20 @@ impl Themes {
|
|||||||
/// Existing themes are overwritten if `force` is `true` or the new theme differs.
|
/// Existing themes are overwritten if `force` is `true` or the new theme differs.
|
||||||
pub(crate) fn register<S: WidgetStyle + 'static>(
|
pub(crate) fn register<S: WidgetStyle + 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
theme: impl ThemeStyle<S> + Send + Sync + 'static,
|
theme: impl StyleProvider<S> + Send + Sync + 'static,
|
||||||
force: bool,
|
force: bool,
|
||||||
) {
|
) {
|
||||||
if !force
|
if !force
|
||||||
&& self
|
&& self
|
||||||
.themes
|
.themes
|
||||||
.get_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(Id::NULL)
|
.get_temp::<Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>>(Id::NULL)
|
||||||
.is_some_and(|t| t.lock().theme_type_id() == theme.theme_type_id())
|
.is_some_and(|t| t.lock().theme_type_id() == theme.theme_type_id())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.themes
|
self.themes
|
||||||
.insert_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(
|
.insert_temp::<Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>>(
|
||||||
Id::NULL,
|
Id::NULL,
|
||||||
Arc::new(Mutex::new(Box::new(theme))),
|
Arc::new(Mutex::new(Box::new(theme))),
|
||||||
);
|
);
|
||||||
@@ -283,16 +297,11 @@ impl Themes {
|
|||||||
/// Fetch the style of the current theme
|
/// Fetch the style of the current theme
|
||||||
pub fn get<S: WidgetStyle + 'static>(
|
pub fn get<S: WidgetStyle + 'static>(
|
||||||
&self,
|
&self,
|
||||||
ui: &Ui,
|
) -> Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>> {
|
||||||
classes: &Classes,
|
|
||||||
state: WidgetState,
|
|
||||||
) -> S {
|
|
||||||
let v = self
|
let v = self
|
||||||
.themes
|
.themes
|
||||||
.get_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(Id::NULL);
|
.get_temp::<Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>>(Id::NULL);
|
||||||
|
|
||||||
v.unwrap_or_else(|| panic!("A style should be set for {:?}", std::any::type_name::<S>()))
|
v.unwrap_or_else(|| panic!("A style should be set for {:?}", std::any::type_name::<S>()))
|
||||||
.lock()
|
|
||||||
.style(ui, classes, state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
|||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Frame, Response, TextBuffer as _,
|
Context, Frame, Response, Style, TextBuffer as _, UiStack,
|
||||||
style::{WidgetVisuals, Widgets},
|
style::{WidgetVisuals, Widgets},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,7 +235,15 @@ pub trait HasClasses {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The list of class
|
/// The list of class
|
||||||
fn list(&self) -> Vec<ClassName> {
|
fn as_slice(&self) -> &[ClassName] {
|
||||||
self.classes().classes.to_vec()
|
&self.classes().classes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StyleArgs<'a> {
|
||||||
|
pub classes: &'a Classes,
|
||||||
|
pub state: WidgetState,
|
||||||
|
pub stack: &'a UiStack,
|
||||||
|
pub style: &'a Style,
|
||||||
|
pub ctx: &'a Context,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use eframe::egui::{
|
use eframe::egui::{
|
||||||
Color32, Ui,
|
Color32,
|
||||||
theme_plugin::{ThemeCache, ThemeStyle},
|
theme_plugin::StyleProvider,
|
||||||
widget_style::{BaseStyle, ButtonStyle, Classes, HasClasses as _, WidgetState},
|
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs},
|
||||||
};
|
};
|
||||||
use logos::Logos;
|
use logos::Logos;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ESSEngine {
|
pub struct ESSEngine {
|
||||||
info: HashMap<String, Vec<(String, Value)>>,
|
info: HashMap<String, Vec<(String, Value)>>,
|
||||||
cache: ThemeCache,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ESSEngine {
|
impl ESSEngine {
|
||||||
@@ -76,15 +75,15 @@ impl ESSEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeStyle<ButtonStyle> for ESSEngine {
|
impl StyleProvider<ButtonStyle> for ESSEngine {
|
||||||
fn style(&mut self, ui: &Ui, classes: &Classes, state: WidgetState) -> ButtonStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
|
||||||
self.cache.get(classes, state, || {
|
let StyleArgs { classes, ctx, .. } = modifiers;
|
||||||
let base = ui.get_widget_style::<BaseStyle>(classes, state);
|
let base = ctx.get_widget_style::<BaseStyle>(modifiers);
|
||||||
let mut default = ButtonStyle {
|
let mut default = ButtonStyle {
|
||||||
frame: base.frame,
|
frame: base.frame,
|
||||||
text_style: base.text,
|
text_style: base.text,
|
||||||
};
|
};
|
||||||
for class in classes.list() {
|
for class in classes.as_slice() {
|
||||||
if let Some(properties) = self.info.get(&class.to_string()) {
|
if let Some(properties) = self.info.get(&class.to_string()) {
|
||||||
for (property, value) in properties {
|
for (property, value) in properties {
|
||||||
match property.as_str() {
|
match property.as_str() {
|
||||||
@@ -104,7 +103,6 @@ impl ThemeStyle<ButtonStyle> for ESSEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default
|
default
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use eframe::egui::{
|
use eframe::egui::{
|
||||||
self, Button, Frame, Margin, Panel, UiBuilder,
|
self, Button, Frame, Margin, Panel, UiBuilder,
|
||||||
|
theme_plugin::ThemeCache,
|
||||||
widget_style::{ButtonStyle, HasClasses as _},
|
widget_style::{ButtonStyle, HasClasses as _},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ fn main() -> eframe::Result {
|
|||||||
eframe::run_ui_native("My egui App", options, move |ui, _frame| {
|
eframe::run_ui_native("My egui App", options, move |ui, _frame| {
|
||||||
// Register the theme plugin and which style they implement
|
// Register the theme plugin and which style they implement
|
||||||
if let Ok(engine) = ESSEngine::try_parse(&style_code) {
|
if let Ok(engine) = ESSEngine::try_parse(&style_code) {
|
||||||
ui.add_widget_theme::<ButtonStyle>(engine);
|
ui.add_widget_theme::<ButtonStyle>(ThemeCache::new(engine));
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.scope_builder(UiBuilder::new().with_class("body"), |ui| {
|
ui.scope_builder(UiBuilder::new().with_class("body"), |ui| {
|
||||||
|
|||||||
Reference in New Issue
Block a user