use crate::layout::{Align, FlexDir};
#[derive(Debug, Clone)]
pub struct Widget {
pub kind: WidgetKind,
pub id: Option<String>,
pub style: Style,
pub flex_dir: FlexDir,
pub children: Vec<Widget>,
pub on_click: Option<String>,
pub enabled: bool,
pub focused: bool,
pub action: Option<Action>,
pub visible: bool,
pub state: StateFlags,
pub states: Option<Box<StateStyles>>,
pub tooltip: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Action {
pub owner: u16,
pub slot: u16,
pub index: u32,
}
impl Action {
pub const SELECT: u16 = u16::MAX;
pub const fn new(slot: u16) -> Self {
Self {
owner: 0,
slot,
index: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct StateFlags(pub u8);
impl StateFlags {
pub const NONE: Self = Self(0);
pub const HOVER: Self = Self(1);
pub const PRESSED: Self = Self(2);
pub const FOCUSED: Self = Self(4);
pub const SELECTED: Self = Self(8);
pub const CHECKED: Self = Self(16);
pub const DISABLED: Self = Self(32);
pub const fn contains(self, o: Self) -> bool {
self.0 & o.0 == o.0
}
pub fn insert(&mut self, o: Self) {
self.0 |= o.0;
}
pub fn remove(&mut self, o: Self) {
self.0 &= !o.0;
}
pub fn set(&mut self, o: Self, on: bool) {
if on { self.insert(o) } else { self.remove(o) }
}
pub const fn union(self, o: Self) -> Self {
Self(self.0 | o.0)
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
}
impl std::ops::BitOr for StateFlags {
type Output = Self;
fn bitor(self, o: Self) -> Self {
self.union(o)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StylePatch {
pub bg: Option<u32>,
pub bg_image: Option<String>,
pub color: Option<u32>,
pub shadow: Option<bool>,
pub focus_color: Option<u32>,
}
impl StylePatch {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
fn apply(&self, s: &mut Style) {
if let Some(v) = self.bg {
s.bg = v;
}
if let Some(v) = &self.bg_image {
s.bg_image.clone_from(v);
}
if let Some(v) = self.color {
s.color = v;
}
if let Some(v) = self.shadow {
s.text_shadow = v;
}
if let Some(v) = self.focus_color {
s.focus_color = v;
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StateStyles {
pub rules: Vec<(StateFlags, StylePatch)>,
}
#[derive(Debug, Clone)]
pub enum WidgetKind {
Panel(FlexDir),
Grid(Box<GridSpec>),
Label(String),
Button(String),
ItemSlot(String),
InvSlot { item_id: String, count: u32 },
Image(Image),
Spacer,
Surface(String),
TextBox(TextBox),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GridLength {
Auto,
Px(f32),
Star(f32),
}
impl GridLength {
pub const STAR: Self = Self::Star(1.0);
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Track {
pub size: GridLength,
pub min: f32,
pub max: f32,
}
impl Track {
pub const fn new(size: GridLength) -> Self {
Self {
size,
min: 0.0,
max: f32::INFINITY,
}
}
pub const fn auto() -> Self {
Self::new(GridLength::Auto)
}
pub const fn px(px: f32) -> Self {
Self::new(GridLength::Px(px))
}
pub const fn star(weight: f32) -> Self {
Self::new(GridLength::Star(weight))
}
pub const fn min(mut self, v: f32) -> Self {
self.min = v;
self
}
pub const fn max(mut self, v: f32) -> Self {
self.max = v;
self
}
}
impl From<GridLength> for Track {
fn from(size: GridLength) -> Self {
Self::new(size)
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct GridSpec {
pub rows: Vec<Track>,
pub cols: Vec<Track>,
pub row_spacing: f32,
pub col_spacing: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub row: u16,
pub col: u16,
pub row_span: u16,
pub col_span: u16,
}
impl Default for Cell {
fn default() -> Self {
Self {
row: 0,
col: 0,
row_span: 1,
col_span: 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HAlign {
Left,
Center,
Right,
Stretch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VAlign {
Top,
Center,
Bottom,
Stretch,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Image {
pub source: String,
pub stretch: Stretch,
pub region: Option<[f32; 4]>,
pub tint: u32,
}
pub use yog_gfx::image::Stretch;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TextBox {
pub text: String,
pub placeholder: String,
pub max_length: usize,
pub on_change: Option<String>,
pub on_submit: Option<String>,
pub caret: Option<usize>,
pub caret_visible: bool,
pub selection: (usize, usize),
pub first: usize,
}
pub const SLOT_SIZE: f32 = 18.0;
pub const DEFAULT_FOCUS_COLOR: u32 = 0xFF_FFE040;
pub(crate) const TEXT_BOX_PAD_X: f32 = 4.0;
pub const PLACEHOLDER_COLOR: u32 = 0xFF_707070;
pub const TEXT_BOX_BORDER: u32 = 0xFF_A0A0A0;
pub const TEXT_BOX_FOCUS_BORDER: u32 = 0xFF_FFFFFF;
pub const SELECTION_COLOR: u32 = 0xFF_2850A0;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Dock {
#[default]
None,
Fill,
Left,
Right,
Top,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FocusStyle {
#[default]
Outline,
Fill,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Units {
#[default]
Auto,
Px(f32),
Percent(f32),
}
impl From<f32> for Units {
fn from(v: f32) -> Self {
if v <= 0.0 { Units::Auto } else { Units::Px(v) }
}
}
impl Units {
pub fn resolve(self, avail: f32) -> Option<f32> {
match self {
Units::Auto => None,
Units::Px(v) => Some(v),
Units::Percent(p) => Some(avail * p / 100.0),
}
}
pub fn is_auto(self) -> bool {
matches!(self, Units::Auto)
}
}
#[derive(Debug, Clone)]
pub struct Style {
pub w: Units, pub h: Units, pub min_w: f32,
pub min_h: f32,
pub flex: f32, pub dock: Dock, pub gap: f32, pub pad: [f32; 4], pub margin: [f32; 4], pub cell: Cell,
pub halign: Option<HAlign>,
pub valign: Option<VAlign>,
pub bg: u32, pub bg_image: String,
pub color: u32, pub align: Align,
pub font_scale: f32,
pub text_shadow: bool, pub focus_style: FocusStyle,
pub focus_color: u32, pub no_wrap: bool,
pub pos: Option<(f32, f32)>,
pub ellipsis: bool,
pub clip: bool,
pub scroll_y: f32,
pub scrollbar: bool,
pub font: String,
}
impl Default for Style {
fn default() -> Self {
Self {
w: Units::Auto,
h: Units::Auto,
min_w: 4.0,
min_h: 4.0,
flex: 0.0,
dock: Dock::None,
gap: 2.0,
pad: [0.0; 4],
margin: [0.0; 4],
cell: Cell::default(),
halign: None,
valign: None,
bg: 0,
bg_image: String::new(),
color: 0xFF_CCCCAA,
align: Align::Start,
font_scale: 1.0,
text_shadow: true,
focus_style: FocusStyle::default(),
focus_color: 0,
no_wrap: false,
pos: None,
ellipsis: false,
clip: false,
scroll_y: 0.0,
scrollbar: false,
font: String::new(),
}
}
}
impl Widget {
fn new(kind: WidgetKind) -> Self {
let flex_dir = if let WidgetKind::Panel(dir) = &kind {
*dir
} else {
FlexDir::Column
};
Self {
kind,
id: None,
style: Style::default(),
flex_dir,
children: Vec::new(),
on_click: None,
enabled: true,
focused: false,
action: None,
visible: true,
state: StateFlags::NONE,
states: None,
tooltip: None,
}
}
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.set_tooltip(text);
self
}
pub fn set_tooltip(&mut self, text: impl Into<String>) {
let t = text.into();
self.tooltip = (!t.is_empty()).then_some(t);
}
pub fn action(mut self, a: Action) -> Self {
self.action = Some(a);
self
}
pub fn visible(mut self, v: bool) -> Self {
self.visible = v;
self
}
pub fn state_style(mut self, when: StateFlags, patch: StylePatch) -> Self {
self.states
.get_or_insert_with(Default::default)
.rules
.push((when, patch));
self
}
pub fn get_path(&self, path: &[u16]) -> Option<&Widget> {
let mut w = self;
for &i in path {
w = w.children.get(i as usize)?;
}
Some(w)
}
pub fn get_path_mut(&mut self, path: &[u16]) -> Option<&mut Widget> {
let mut w = self;
for &i in path {
w = w.children.get_mut(i as usize)?;
}
Some(w)
}
pub fn at_path(&self, path: &[u16]) -> &Widget {
self.get_path(path)
.unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
}
pub fn at_path_mut(&mut self, path: &[u16]) -> &mut Widget {
self.get_path_mut(path)
.unwrap_or_else(|| panic!("yog-ui: no widget at path {path:?}"))
}
pub fn is_interactive(&self) -> bool {
self.action.is_some() || self.on_click.is_some()
}
pub fn set_state_inherited(&mut self, mask: StateFlags, on: bool) {
self.state.set(mask, on);
for c in &mut self.children {
if !c.is_interactive() {
c.set_state_inherited(mask, on);
}
}
}
pub fn clear_state_deep(&mut self, mask: StateFlags) {
self.state.remove(mask);
for c in &mut self.children {
c.clear_state_deep(mask);
}
}
pub fn effective_style(&self) -> std::borrow::Cow<'_, Style> {
let mut state = self.state;
if !self.enabled {
state.insert(StateFlags::DISABLED);
}
self.effective_style_in(state)
}
pub fn effective_style_in(&self, state: StateFlags) -> std::borrow::Cow<'_, Style> {
let Some(states) = &self.states else {
return std::borrow::Cow::Borrowed(&self.style);
};
let mut matching = states
.rules
.iter()
.filter(|(when, _)| !when.is_empty() && state.contains(*when))
.peekable();
if matching.peek().is_none() {
return std::borrow::Cow::Borrowed(&self.style);
}
let mut s = self.style.clone();
for (_, patch) in matching {
patch.apply(&mut s);
}
std::borrow::Cow::Owned(s)
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn on_click(mut self, ev: impl Into<String>) -> Self {
self.on_click = Some(ev.into());
self
}
pub fn child(mut self, w: Widget) -> Self {
self.children.push(w);
self
}
pub fn w(mut self, w: impl Into<Units>) -> Self {
self.style.w = w.into();
self
}
pub fn h(mut self, h: impl Into<Units>) -> Self {
self.style.h = h.into();
self
}
pub fn min_w(mut self, v: f32) -> Self {
self.style.min_w = v;
self
}
pub fn min_h(mut self, v: f32) -> Self {
self.style.min_h = v;
self
}
pub fn flex(mut self, v: f32) -> Self {
self.style.flex = v;
self
}
pub fn flex_dir(mut self, v: FlexDir) -> Self {
self.flex_dir = v;
self
}
pub fn gap(mut self, v: f32) -> Self {
self.style.gap = v;
self
}
pub fn bg(mut self, v: u32) -> Self {
self.style.bg = v;
self
}
pub fn bg_image(mut self, source: impl Into<String>) -> Self {
self.style.bg_image = source.into();
self
}
pub fn color(mut self, v: u32) -> Self {
self.style.color = v;
self
}
pub fn align(mut self, v: Align) -> Self {
self.style.align = v;
self
}
pub fn font_scale(mut self, v: f32) -> Self {
self.style.font_scale = v;
self
}
pub fn shadow(mut self, v: bool) -> Self {
self.style.text_shadow = v;
self
}
pub fn no_wrap(mut self) -> Self {
self.style.no_wrap = true;
self
}
pub fn focus_style(mut self, v: FocusStyle) -> Self {
self.style.focus_style = v;
self
}
pub fn focus_color(mut self, v: u32) -> Self {
self.style.focus_color = v;
self
}
pub fn dock(mut self, v: Dock) -> Self {
self.style.dock = v;
self
}
pub fn enabled(mut self, v: bool) -> Self {
self.enabled = v;
self
}
pub fn focused(mut self, v: bool) -> Self {
self.focused = v;
self
}
pub fn at(mut self, x: f32, y: f32) -> Self {
self.style.pos = Some((x, y));
self
}
pub fn ellipsis(mut self) -> Self {
self.style.no_wrap = true;
self.style.ellipsis = true;
self
}
pub fn clip(mut self, v: bool) -> Self {
self.style.clip = v;
self
}
pub fn scroll_y(mut self, v: f32) -> Self {
self.style.scroll_y = v;
self
}
pub fn scrollbar(mut self, v: bool) -> Self {
self.style.scrollbar = v;
self
}
pub fn font(mut self, font: impl Into<String>) -> Self {
self.style.font = font.into();
self
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
if let WidgetKind::TextBox(t) = &mut self.kind {
t.placeholder = text.into();
}
self
}
pub fn max_length(mut self, n: usize) -> Self {
if let WidgetKind::TextBox(t) = &mut self.kind {
t.max_length = n;
}
self
}
pub fn on_change(mut self, ev: impl Into<String>) -> Self {
if let WidgetKind::TextBox(t) = &mut self.kind {
t.on_change = Some(ev.into());
}
self
}
pub fn on_submit(mut self, ev: impl Into<String>) -> Self {
if let WidgetKind::TextBox(t) = &mut self.kind {
t.on_submit = Some(ev.into());
}
self
}
pub fn stretch(mut self, stretch: Stretch) -> Self {
if let WidgetKind::Image(i) = &mut self.kind {
i.stretch = stretch;
}
self
}
pub fn region(mut self, region: [f32; 4]) -> Self {
if let WidgetKind::Image(i) = &mut self.kind {
i.region = Some(region);
}
self
}
pub fn tint(mut self, tint: u32) -> Self {
if let WidgetKind::Image(i) = &mut self.kind {
i.tint = tint;
}
self
}
pub fn children(mut self, children: impl IntoIterator<Item = Widget>) -> Self {
self.children.extend(children);
self
}
pub fn find_mut(&mut self, id: &str) -> Option<&mut Widget> {
if self.id.as_deref() == Some(id) {
return Some(self);
}
self.children.iter_mut().find_map(|c| c.find_mut(id))
}
pub fn set_text(&mut self, text: impl Into<String>) -> bool {
match &mut self.kind {
WidgetKind::Label(t) | WidgetKind::Button(t) => {
*t = text.into();
true
}
WidgetKind::TextBox(t) => {
t.text = text.into();
true
}
_ => false,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.set_text(text);
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
match &mut self.kind {
WidgetKind::Image(i) => i.source = source.into(),
WidgetKind::Surface(id) => *id = source.into(),
WidgetKind::ItemSlot(id) | WidgetKind::InvSlot { item_id: id, .. } => {
*id = source.into()
}
_ => {}
}
self
}
pub fn with_count(mut self, n: u32) -> Self {
if let WidgetKind::InvSlot { count, .. } = &mut self.kind {
*count = n;
}
self
}
pub fn padding(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
self.style.pad = [top, right, bottom, left];
self
}
pub fn margin(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
self.style.margin = [top, right, bottom, left];
self
}
pub fn cell(mut self, row: u16, col: u16) -> Self {
self.style.cell.row = row;
self.style.cell.col = col;
self
}
pub fn span(mut self, rows: u16, cols: u16) -> Self {
self.style.cell.row_span = rows.max(1);
self.style.cell.col_span = cols.max(1);
self
}
pub fn halign(mut self, v: HAlign) -> Self {
self.style.halign = Some(v);
self
}
pub fn valign(mut self, v: VAlign) -> Self {
self.style.valign = Some(v);
self
}
pub fn rows(mut self, rows: impl IntoIterator<Item = impl Into<Track>>) -> Self {
if let WidgetKind::Grid(g) = &mut self.kind {
g.rows = rows.into_iter().map(Into::into).collect();
}
self
}
pub fn cols(mut self, cols: impl IntoIterator<Item = impl Into<Track>>) -> Self {
if let WidgetKind::Grid(g) = &mut self.kind {
g.cols = cols.into_iter().map(Into::into).collect();
}
self
}
pub fn row_spacing(mut self, v: f32) -> Self {
if let WidgetKind::Grid(g) = &mut self.kind {
g.row_spacing = v;
}
self
}
pub fn col_spacing(mut self, v: f32) -> Self {
if let WidgetKind::Grid(g) = &mut self.kind {
g.col_spacing = v;
}
self
}
}
pub fn panel(dir: FlexDir) -> Widget {
Widget::new(WidgetKind::Panel(dir))
}
pub fn grid() -> Widget {
Widget::new(WidgetKind::Grid(Box::default()))
}
pub fn scroll_view() -> Widget {
panel(FlexDir::Column).clip(true).scrollbar(true)
}
pub fn label(text: impl Into<String>) -> Widget {
Widget::new(WidgetKind::Label(text.into()))
}
pub fn button(text: impl Into<String>) -> Widget {
Widget::new(WidgetKind::Button(text.into()))
}
pub fn item_slot(item_id: impl Into<String>) -> Widget {
Widget::new(WidgetKind::ItemSlot(item_id.into()))
}
pub fn inv_slot(item_id: impl Into<String>, count: u32) -> Widget {
Widget::new(WidgetKind::InvSlot {
item_id: item_id.into(),
count,
})
.w(SLOT_SIZE)
.h(SLOT_SIZE)
}
pub const SLOT_ID_PREFIX: &str = "yog:slot:";
pub const PLAYER_INVENTORY_ID: &str = "yog:player_inventory";
pub const HOTBAR_GAP: f32 = 4.0;
pub fn slot(index: usize) -> Widget {
inv_slot("", 0).id(format!("{SLOT_ID_PREFIX}{index}"))
}
pub fn slot_accepting(index: usize, accepts: &[&str]) -> Widget {
if accepts.is_empty() {
return slot(index);
}
inv_slot("", 0).id(format!("{SLOT_ID_PREFIX}{index}={}", accepts.join("|")))
}
pub fn player_inventory() -> Widget {
let row = || (0..9).fold(panel(FlexDir::Row).gap(0.0), |r, _| r.child(inv_slot("", 0)));
panel(FlexDir::Column)
.id(PLAYER_INVENTORY_ID)
.gap(0.0)
.child(row())
.child(row())
.child(row())
.child(row().margin(HOTBAR_GAP, 0.0, 0.0, 0.0))
}
pub fn image(source: impl Into<String>) -> Widget {
Widget::new(WidgetKind::Image(Image {
source: source.into(),
stretch: Stretch::Uniform,
region: None,
tint: 0xFFFF_FFFF,
}))
}
pub fn spacer() -> Widget {
Widget::new(WidgetKind::Spacer)
}
pub fn surface(id: impl Into<String>) -> Widget {
Widget::new(WidgetKind::Surface(id.into()))
}
pub fn text_box(text: impl Into<String>) -> Widget {
Widget::new(WidgetKind::TextBox(TextBox {
text: text.into(),
..TextBox::default()
}))
.bg(0xFF_000000)
.color(0xFF_E0E0E0)
.padding(0.0, TEXT_BOX_PAD_X, 0.0, TEXT_BOX_PAD_X)
}