wgui: Add `ColorSelector` component

This commit is contained in:
Aleksander 2026-04-11 00:22:24 +02:00
parent 5eb258e310
commit 0c1b0906c1
21 changed files with 418 additions and 38 deletions

View File

@ -485,7 +485,6 @@ impl<T: 'static> Frontend<T> {
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
self.window_audio_settings.open(&mut WguiWindowParams {
globals: &self.globals,
position: Vec2::new(64.0, 64.0),
layout: &mut self.layout,
extra: WguiWindowParamsExtra {

View File

@ -267,7 +267,6 @@ impl SubtabGeneralSettings {
let tasks = tasks.clone();
Box::new(move |_common, e| {
tasks.push(Task::SetBrightness(e.value / 100.0));
Ok(())
})
});
}

View File

@ -118,7 +118,6 @@ pub fn options_slider_f32(
let tasks = mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateFloat(setting, e.value));
Ok(())
}
}));
@ -168,7 +167,6 @@ pub fn options_slider_i32(
let tasks = mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateInt(setting, e.value as i32));
Ok(())
}
}));
Ok(())

View File

@ -823,8 +823,10 @@ impl View {
slider.on_value_changed({
let control = params.control.clone();
Box::new(move |_common, event| {
control.on_volume_change(event.value * VOLUME_MULT)?;
Ok(())
if let Err(e) = control.on_volume_change(event.value * VOLUME_MULT) {
log::error!("{:?}", e);
debug_assert!(false);
};
})
});

View File

@ -92,4 +92,4 @@
</div>
</rectangle>
</elements>
</layout>
</layout>

View File

@ -59,7 +59,11 @@
</div>
</rectangle>
<div>
<div gap="4" flex_direction="column">
<rectangle macro="rect">
<label text="Color selector!"/>
<ColorSelector />
</rectangle>
<rectangle macro="rect">
<label text="Context menu test" />
<Button id="button_context_menu" text="Show context menu" />
@ -121,4 +125,4 @@
</rectangle>
</div>
</elements>
</layout>
</layout>

View File

@ -229,7 +229,6 @@ impl TestbedGeneric {
data: &mut Data,
) -> anyhow::Result<()> {
data.popup_window.open(&mut WguiWindowParams {
globals: &self.globals,
position: Vec2::new(128.0, 128.0),
layout: &mut self.layout,
extra: WguiWindowParamsExtra {

View File

@ -674,7 +674,6 @@ fn set_up_slider(
OverlaySelector::Id(*overlay_id.borrow()),
Box::new(move |app, owc| callback(app, owc, e_value)),
)));
Ok(())
}));
Ok(())

View File

@ -0,0 +1,25 @@
<layout>
<macro name="slider" min_value="0" max_value="255" width="200" height="24" />
<macro name="color_pip" width="16" height="16" round="100%" />
<macro name="slider_flow" align_items="center" gap="8" />
<elements>
<div gap="8" align_items="center">
<div gap="8" flex_direction="column">
<div macro="slider_flow">
<rectangle macro="color_pip" color="#FF0000"/>
<Slider id="slider_r" macro="slider" />
</div>
<div macro="slider_flow">
<rectangle macro="color_pip" color="#00FF00"/>
<Slider id="slider_g" macro="slider" />
</div>
<div macro="slider_flow">
<rectangle macro="color_pip" color="#0000FF"/>
<Slider id="slider_b" macro="slider" />
</div>
</div>
<rectangle id="rect_color" width="96" height="96" border="4" color="#ffffff" border_color="#000000" round="4" />
</div>
</elements>
</layout>

View File

@ -70,7 +70,11 @@ impl ComponentTrait for ComponentBarGraph {
fn refresh(&self, data: &mut RefreshData) {
let state = self.state.borrow();
self.update_limits_text(&state, data.common);
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
self.update_limits_text(&state, &mut lc.common());
let _ = lc.finish();
}
}

View File

@ -123,15 +123,21 @@ impl ComponentTrait for ComponentButton {
fn refresh(&self, data: &mut RefreshData) {
let mut state = self.state.borrow_mut();
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
if state.active_tooltip.is_some() {
if let Some(node_id) = data.common.state.nodes.get(self.base.get_id()) {
if !widget::is_node_visible(&data.common.state.tree, *node_id) {
let common = lc.common();
if let Some(node_id) = common.state.nodes.get(self.base.get_id()) {
if !widget::is_node_visible(&common.state.tree, *node_id) {
state.active_tooltip = None; // destroy the tooltip, this button is now hidden
}
} else {
debug_assert!(false);
}
}
let _ = lc.finish();
}
}
@ -139,6 +145,10 @@ fn get_color2(color: &drawing::Color, gradient_intensity: f32) -> drawing::Color
color.lerp(&Color::new(0.0, 0.0, 0.0, color.a), gradient_intensity)
}
fn get_hover_color(color: &drawing::Color) -> drawing::Color {
Color::new(color.r + 0.25, color.g + 0.25, color.g + 0.25, color.a + 0.15)
}
impl ComponentButton {
pub fn get_label(&self) -> WidgetID {
self.data.id_label
@ -165,7 +175,7 @@ impl ComponentButton {
let mut state = self.state.borrow_mut();
state.colors.color = color;
state.colors.hover_color = get_hover_color(&color);
rect.params.color = color;
rect.params.color2 = get_color2(&color, gradient_intensity);
}
@ -435,9 +445,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
.border_color
.unwrap_or_else(|| Color::new(color.r, color.g, color.b, color.a + 0.25));
let hover_color = params
.hover_color
.unwrap_or_else(|| Color::new(color.r + 0.25, color.g + 0.25, color.g + 0.25, color.a + 0.15));
let hover_color = params.hover_color.unwrap_or_else(|| get_hover_color(&color));
let hover_border_color = params
.hover_border_color

View File

@ -0,0 +1,299 @@
use crate::{
assets::AssetPath,
components::{
Component, ComponentBase, ComponentTrait, RefreshData,
button::{self, ComponentButton},
slider::{ComponentSlider, SliderValueChangedCallback},
},
drawing::{self},
event::CallbackDataCommon,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID, WidgetPair},
parser::{self, Fetchable, ParseDocumentParams, ParserState},
widget::{ConstructEssentials, rectangle::WidgetRectangle, util::WLength},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
};
use glam::Vec2;
use std::{
cell::RefCell,
rc::{Rc, Weak},
};
use taffy::prelude::length;
pub struct Params {
pub color: drawing::Color,
pub style: taffy::Style,
}
impl Default for Params {
fn default() -> Self {
Self {
color: drawing::Color::new(1.0, 1.0, 1.0, 1.0),
style: Default::default(),
}
}
}
struct WantsOpen {
position: Vec2,
}
#[allow(dead_code)]
struct PopupState {
slider_r: Rc<ComponentSlider>,
slider_g: Rc<ComponentSlider>,
slider_b: Rc<ComponentSlider>,
id_rect_color: WidgetID,
}
struct State {
color: drawing::Color,
self_ref: Weak<ComponentColorSelector>,
wants_open: Option<WantsOpen>,
on_changed: Option<ColorSelectorChangedCallback>,
popup_state: Option<PopupState>,
}
struct Data {
button: Rc<ComponentButton>,
}
pub struct ColorSelectorChangedEvent {
pub color: drawing::Color,
}
pub type ColorSelectorChangedCallback = Box<dyn Fn(&mut CallbackDataCommon, ColorSelectorChangedEvent)>;
pub struct ComponentColorSelector {
base: ComponentBase,
data: Rc<Data>,
state: Rc<RefCell<State>>,
window: WguiWindow,
}
impl ComponentTrait for ComponentColorSelector {
fn base(&self) -> &ComponentBase {
&self.base
}
fn base_mut(&mut self) -> &mut ComponentBase {
&mut self.base
}
fn refresh(&self, data: &mut RefreshData) {
let mut state = self.state.borrow_mut();
if let Some(wants_open) = state.wants_open.take() {
if let Err(e) = self.open(data.layout, &mut state, wants_open.position) {
log::error!("{:?}", e);
debug_assert!(false);
}
}
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
let mut common = lc.common();
self.data.button.set_text(
&mut common,
Translation::from_raw_text_string(format!("{}", state.color.to_hex_rgb())),
);
self.data.button.set_color(&mut common, state.color);
let _ = lc.finish();
}
}
enum ColorIndex {
Red,
Green,
Blue,
}
fn set_color_internal(state: &mut State, common: &mut CallbackDataCommon, color: drawing::Color) {
if state.color == color {
return;
}
if let Some(on_changed) = &state.on_changed {
on_changed(common, ColorSelectorChangedEvent { color })
}
state.color = color;
common.alterables.refresh_component_once(&state.self_ref);
common.alterables.mark_redraw();
}
impl ComponentColorSelector {
pub fn on_changed(&self, func: ColorSelectorChangedCallback) {
self.state.borrow_mut().on_changed = Some(func);
}
pub fn set_color(&self, common: &mut CallbackDataCommon, color: drawing::Color) {
let mut state = self.state.borrow_mut();
set_color_internal(&mut state, common, color);
}
pub fn get_color(&self) -> drawing::Color {
self.state.borrow().color
}
fn open(&self, layout: &mut Layout, state: &mut State, position: Vec2) -> anyhow::Result<()> {
self.window.open(&mut WguiWindowParams {
position,
layout,
extra: WguiWindowParamsExtra {
close_if_clicked_outside: true,
// TODO: translation text in wgui too?
title: Some(Translation::from_raw_text("Select color")),
..Default::default()
},
})?;
let id_content = self.window.get_content().id;
let parser_state = parser::parse_from_assets(
&mut ParseDocumentParams {
globals: layout.state.globals.clone(),
path: AssetPath::WguiInternal("wgui/color_selector.xml"),
extra: Default::default(),
},
layout,
id_content,
)?;
let slider_r = parser_state.fetch_component_as::<ComponentSlider>("slider_r")?;
let slider_g = parser_state.fetch_component_as::<ComponentSlider>("slider_g")?;
let slider_b = parser_state.fetch_component_as::<ComponentSlider>("slider_b")?;
{
let mut lc = layout.start_common();
let common = &mut lc.common();
slider_r.set_value(common, state.color.r * 255.0);
slider_g.set_value(common, state.color.g * 255.0);
slider_b.set_value(common, state.color.b * 255.0);
}
slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red));
slider_g.on_value_changed(self.gen_slider_callback(ColorIndex::Green));
slider_b.on_value_changed(self.gen_slider_callback(ColorIndex::Blue));
let id_rect_color = parser_state.get_widget_id("rect_color")?;
state.popup_state = Some(PopupState {
slider_r,
slider_g,
slider_b,
id_rect_color,
});
Ok(())
}
fn gen_slider_callback(&self, idx: ColorIndex) -> SliderValueChangedCallback {
let state = Rc::downgrade(&self.state);
Box::new(move |common, evt| {
let Some(state) = state.upgrade() else {
return;
};
let mut state = state.borrow_mut();
let Some(popup_state) = &state.popup_state else {
return;
};
let norm = evt.value / 255.0;
let mut new_color = state.color;
match idx {
ColorIndex::Red => new_color.r = norm,
ColorIndex::Green => new_color.g = norm,
ColorIndex::Blue => new_color.b = norm,
}
if let Some(mut rect) = common
.state
.widgets
.get_as::<WidgetRectangle>(popup_state.id_rect_color)
{
rect.set_color(common, new_color);
}
set_color_internal(&mut state, common, new_color);
})
}
}
const DEFAULT_WIDTH: f32 = 96.0;
const DEFAULT_HEIGHT: f32 = 32.0;
pub fn construct(
ess: &mut ConstructEssentials,
params: Params,
) -> anyhow::Result<(WidgetPair, Rc<ComponentColorSelector>)> {
let mut style = params.style;
if style.size.width.is_auto() {
style.size.width = length(DEFAULT_WIDTH);
}
if style.size.height.is_auto() {
style.size.height = length(DEFAULT_HEIGHT);
}
style.min_size = style.size;
style.max_size = style.size;
let (widget_button, button) = button::construct(
ess,
button::Params {
color: Some(params.color),
round: WLength::Percent(1.0),
border: 2.0,
border_color: Some(drawing::Color::new(0.0, 0.0, 0.0, 1.0)),
style,
..Default::default()
},
)?;
let data = Rc::new(Data { button: button.clone() });
let state = Rc::new(RefCell::new(State {
color: params.color,
self_ref: Default::default(),
wants_open: None,
popup_state: None,
on_changed: None,
}));
let base = ComponentBase {
id: widget_button.id,
lhandles: Default::default(),
};
let color_selector = Rc::new(ComponentColorSelector {
base,
data,
state: state.clone(),
window: WguiWindow::default(),
});
let self_ref = Rc::downgrade(&color_selector);
state.borrow_mut().self_ref = self_ref.clone();
button.on_click(Rc::new({
let color_selector = color_selector.clone();
move |common, evt| {
let mut state = color_selector.state.borrow_mut();
state.wants_open = Some(WantsOpen {
position: evt.mouse_pos_absolute.unwrap_or_default(),
});
common.alterables.refresh_component_once(&self_ref);
Ok(())
}
}));
ess.layout.defer_component_refresh(Component(color_selector.clone()));
Ok((widget_button, color_selector))
}

View File

@ -147,9 +147,15 @@ impl ComponentTrait for ComponentEditBox {
}
fn refresh(&self, data: &mut RefreshData) {
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
let mut common = lc.common();
let mut state = self.state.borrow_mut();
let res = refresh_all(data.common, &self.data, &mut state);
let res = refresh_all(&mut common, &self.data, &mut state);
debug_assert!(res.is_some());
let _ = lc.finish();
}
fn on_focus_change(&self, data: &mut FocusChangeData) {

View File

@ -1,6 +1,7 @@
use std::hash::Hash;
use std::{hash::Hasher, rc::Rc};
use crate::layout::Layout;
use crate::{
any::AnyTrait,
event::{CallbackDataCommon, EventListenerID},
@ -10,6 +11,7 @@ use crate::{
pub mod bar_graph;
pub mod button;
pub mod checkbox;
pub mod color_selector;
pub mod editbox;
pub mod radio_group;
pub mod slider;
@ -17,7 +19,7 @@ pub mod tabs;
pub mod tooltip;
pub struct RefreshData<'a> {
pub common: &'a mut CallbackDataCommon<'a>,
pub layout: &'a mut Layout,
}
pub struct FocusChangeData<'a> {

View File

@ -101,8 +101,7 @@ pub struct SliderValueChangedEvent {
pub value: f32,
}
pub type SliderValueChangedCallback =
Box<dyn Fn(&mut CallbackDataCommon, SliderValueChangedEvent) -> anyhow::Result<()>>;
pub type SliderValueChangedCallback = Box<dyn Fn(&mut CallbackDataCommon, SliderValueChangedEvent)>;
pub struct ComponentSlider {
base: ComponentBase,
@ -111,10 +110,14 @@ pub struct ComponentSlider {
}
impl ComponentTrait for ComponentSlider {
fn refresh(&self, init_data: &mut RefreshData) {
fn refresh(&self, data: &mut RefreshData) {
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
let mut common = lc.common();
let mut state = self.state.borrow_mut();
let value = state.values.value;
state.set_value(init_data.common, &self.data, value);
state.set_value(&mut common, &self.data, value);
let _ = lc.finish();
}
fn base(&self) -> &ComponentBase {
@ -250,15 +253,13 @@ impl State {
Self::update_text(common, &mut label, self.values.value);
}
if changed
&& let Some(on_value_changed) = &self.on_value_changed
&& let Err(e) = on_value_changed(
if changed && let Some(on_value_changed) = &self.on_value_changed {
on_value_changed(
common,
SliderValueChangedEvent {
value: self.values.value,
},
) {
log::error!("{e:?}"); // FIXME: proper error handling
)
}
}
}

View File

@ -102,7 +102,7 @@ impl Boundary {
}
}
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
@ -179,6 +179,14 @@ impl Color {
format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
}
#[must_use]
pub fn to_hex_rgb(&self) -> String {
let r = (self.r.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (self.g.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (self.b.clamp(0.0, 1.0) * 255.0).round() as u8;
format!("#{r:02X}{g:02X}{b:02X}")
}
#[must_use]
pub const fn as_arr(&self) -> [f32; 4] {
[self.r, self.b, self.g, self.a]

View File

@ -339,15 +339,14 @@ impl Layout {
}
fn process_pending_components(&mut self, alterables: &mut EventAlterables) {
for comp in &self.components_to_refresh_once {
for comp in std::mem::take(&mut self.components_to_refresh_once) {
let mut common = CallbackDataCommon {
state: &self.state,
alterables,
};
comp.0.refresh(&mut RefreshData { common: &mut common });
comp.0.refresh(&mut RefreshData { layout: self });
}
self.components_to_refresh_once.clear();
}
fn process_pending_widget_ticks(&mut self, alterables: &mut EventAlterables) {

View File

@ -0,0 +1,25 @@
use crate::{
components::{Component, color_selector},
drawing,
layout::WidgetID,
parser::{AttribPair, ParserContext, process_component, style::parse_style},
};
pub fn parse_component_color_selector(
ctx: &mut ParserContext,
parent_id: WidgetID,
attribs: &[AttribPair],
tag_name: &str,
) -> anyhow::Result<WidgetID> {
let style = parse_style(ctx, attribs, tag_name);
let color = drawing::Color::new(1.0, 1.0, 1.0, 1.0);
let (widget, component) = color_selector::construct(
&mut ctx.get_construct_essentials(parent_id),
color_selector::Params { color, style },
)?;
process_component(ctx, Component(component), widget.id, attribs);
Ok(widget.id)
}

View File

@ -1,6 +1,7 @@
mod component_bar_graph;
mod component_button;
mod component_checkbox;
mod component_color_selector;
mod component_editbox;
mod component_radio_group;
mod component_slider;
@ -25,6 +26,7 @@ use crate::{
component_bar_graph::parse_component_bar_graph,
component_button::parse_component_button,
component_checkbox::{CheckboxKind, parse_component_checkbox},
component_color_selector::parse_component_color_selector,
component_editbox::parse_component_editbox,
component_radio_group::parse_component_radio_group,
component_slider::parse_component_slider,
@ -1063,6 +1065,7 @@ fn parse_child<'a>(
"Slider" => {
new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs, tag_name)?);
}
"ColorSelector" => new_widget_id = Some(parse_component_color_selector(ctx, parent_id, &attribs, tag_name)?),
"CheckBox" => {
new_widget_id = Some(parse_component_checkbox(
ctx,

View File

@ -94,7 +94,6 @@ impl ContextMenu {
let globals = layout.state.globals.clone();
self.window.open(&mut WguiWindowParams {
globals: &globals,
layout,
position: params.position,
extra: WguiWindowParamsExtra {

View File

@ -81,7 +81,6 @@ impl Default for WguiWindowParamsExtra {
pub struct WguiWindowParams<'a> {
pub position: Vec2,
pub globals: &'a WguiGlobals,
pub layout: &'a mut Layout,
pub extra: WguiWindowParamsExtra,
}
@ -100,7 +99,7 @@ impl WguiWindow {
self.0.borrow_mut().opened_window = None;
}
pub fn open(&mut self, params: &mut WguiWindowParams) -> anyhow::Result<()> {
pub fn open(&self, params: &mut WguiWindowParams) -> anyhow::Result<()> {
// close previous one if it's already open
self.close();
@ -216,12 +215,14 @@ impl WguiWindow {
},
)?;
let globals = params.layout.state.globals.clone();
let content_id = if params.extra.with_decorations {
let xml_path: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml");
let state = parser::parse_from_assets(
&parser::ParseDocumentParams {
globals: params.globals.clone(),
globals: globals.clone(),
path: xml_path,
extra: Default::default(),
},
@ -240,7 +241,7 @@ impl WguiWindow {
if let Some(title) = &params.extra.title {
let mut text_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "text_window_title")?;
text_title.set_text_simple(&mut params.globals.get(), title.clone());
text_title.set_text_simple(&mut globals.get(), title.clone());
}
let content = state.fetch_widget(&params.layout.state, "content")?;