use crate::*;
use std::rc::Rc;
fn text_size(style: &Style, atlas: &AtlasHandle, font: FontChoice, text: &str) -> Dimensioni {
atlas.get_text_size(style.resolve_font_choice(font), text)
}
fn content_height(style: &Style, atlas: &AtlasHandle, font: FontChoice, visual_height: i32) -> i32 {
let font_height = atlas.get_font_height(style.resolve_font_choice(font)) as i32;
let vertical_pad = (style.padding / 2).max(1);
(font_height.max(visual_height) + vertical_pad * 2).max(0)
}
fn widget_fill_color(control: &ControlState, base: ControlColor, fill: WidgetFillOption) -> Option<ControlColor> {
if control.focused && fill.fill_click() {
let mut color = base;
color.focus();
Some(color)
} else if control.hovered && fill.fill_hover() {
let mut color = base;
color.hover();
Some(color)
} else if fill.fill_normal() {
Some(base)
} else {
None
}
}
#[derive(Clone)]
pub enum ButtonContent {
Text {
label: String,
icon: Option<IconId>,
},
Image {
label: String,
image: Option<Image>,
},
Slot {
label: String,
slot: SlotId,
paint: Rc<dyn Fn(usize, usize) -> Color4b>,
},
}
#[derive(Clone)]
pub struct Button {
pub content: ButtonContent,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
pub fill: WidgetFillOption,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
content: ButtonContent::Text { label: label.into(), icon: None },
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
fill: WidgetFillOption::ALL,
}
}
pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
Self {
content: ButtonContent::Text { label: label.into(), icon: None },
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
fill: WidgetFillOption::ALL,
}
}
pub fn with_image(label: impl Into<String>, image: Option<Image>, opt: WidgetOption, fill: WidgetFillOption) -> Self {
Self {
content: ButtonContent::Image { label: label.into(), image },
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
fill,
}
}
pub fn with_slot(label: impl Into<String>, slot: SlotId, paint: Rc<dyn Fn(usize, usize) -> Color4b>, opt: WidgetOption, fill: WidgetFillOption) -> Self {
Self {
content: ButtonContent::Slot { label: label.into(), slot, paint },
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
fill,
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let mut width = padding * 2;
let mut visual_w = 0;
let mut visual_h = 0;
let mut text_w = 0;
let mut has_text = false;
match &self.content {
ButtonContent::Text { label, icon } => {
if !label.is_empty() {
has_text = true;
text_w = text_size(style, atlas, self.font, label).width;
}
if let Some(icon) = icon {
let size = atlas.get_icon_size(*icon);
visual_w = size.width;
visual_h = size.height;
}
}
ButtonContent::Image { label, image } => {
if !label.is_empty() {
has_text = true;
text_w = text_size(style, atlas, self.font, label).width;
}
if let Some(Image::Slot(slot)) = image {
let size = atlas.get_slot_size(*slot);
visual_w = size.width;
visual_h = size.height;
}
}
ButtonContent::Slot { label, slot, .. } => {
if !label.is_empty() {
has_text = true;
text_w = text_size(style, atlas, self.font, label).width;
}
let size = atlas.get_slot_size(*slot);
visual_w = size.width;
visual_h = size.height;
}
}
if has_text {
width += text_w.max(0);
}
if visual_w > 0 {
width += visual_w.max(0);
if has_text {
width += padding;
}
}
let height = content_height(style, atlas, self.font, visual_h);
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
if control.clicked {
res |= ResourceState::SUBMIT;
}
let rect = ctx.rect();
if !self.opt.has_no_frame() {
if let Some(colorid) = widget_fill_color(control, ControlColor::Button, self.fill) {
ctx.draw_frame(rect, colorid);
}
}
let font = ctx.style().resolve_font_choice(self.font);
match &self.content {
ButtonContent::Text { label, icon } => {
if !label.is_empty() {
ctx.draw_control_text_with_font(font, label, rect, ControlColor::Text, self.opt);
}
if let Some(icon) = icon {
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.draw_icon(*icon, rect, color);
}
}
ButtonContent::Image { label, image } => {
if !label.is_empty() {
ctx.draw_control_text_with_font(font, label, rect, ControlColor::Text, self.opt);
}
if let Some(image) = *image {
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.push_image(image, rect, color);
}
}
ButtonContent::Slot { label, slot, paint } => {
if !label.is_empty() {
ctx.draw_control_text_with_font(font, label, rect, ControlColor::Text, self.opt);
}
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.draw_slot_with_function(*slot, rect, color, paint.clone());
}
}
res
}
}
implement_widget!(Button, handle_widget, preferred_size_widget);
#[derive(Clone)]
pub struct ListItem {
pub label: String,
pub icon: Option<IconId>,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
}
impl ListItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
icon: None,
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_opt(label: impl Into<String>, opt: WidgetOption) -> Self {
Self {
label: label.into(),
icon: None,
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_icon(label: impl Into<String>, icon: IconId) -> Self {
Self {
label: label.into(),
icon: Some(icon),
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_icon_opt(label: impl Into<String>, icon: IconId, opt: WidgetOption) -> Self {
Self {
label: label.into(),
icon: Some(icon),
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let mut width = padding * 2;
let mut visual_h = 0;
if let Some(icon) = self.icon {
let size = atlas.get_icon_size(icon);
width += size.width + padding;
visual_h = size.height;
}
if !self.label.is_empty() {
width += text_size(style, atlas, self.font, &self.label).width;
}
let height = content_height(style, atlas, self.font, visual_h);
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
let bounds = ctx.rect();
if control.clicked {
res |= ResourceState::SUBMIT;
}
if control.focused || control.hovered {
let mut color = ControlColor::Button;
if control.focused {
color.focus();
} else {
color.hover();
}
let fill = ctx.style().colors[color as usize];
ctx.draw_rect(bounds, fill);
}
let mut text_rect = bounds;
if let Some(icon) = self.icon {
let padding = ctx.style().padding.max(0);
let icon_size = ctx.atlas().get_icon_size(icon);
let icon_x = bounds.x + padding;
let icon_y = bounds.y + ((bounds.height - icon_size.height) / 2).max(0);
let icon_rect = rect(icon_x, icon_y, icon_size.width, icon_size.height);
let consumed = icon_size.width + padding * 2;
text_rect.x += consumed;
text_rect.width = (text_rect.width - consumed).max(0);
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.draw_icon(icon, icon_rect, color);
}
if !self.label.is_empty() {
let font = ctx.style().resolve_font_choice(self.font);
ctx.draw_control_text_with_font(font, &self.label, text_rect, ControlColor::Text, self.opt);
}
res
}
}
implement_widget!(ListItem, handle_widget, preferred_size_widget);
#[derive(Clone)]
pub struct ListBox {
pub label: String,
pub image: Option<Image>,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
}
impl ListBox {
pub fn new(label: impl Into<String>, image: Option<Image>) -> Self {
Self {
label: label.into(),
image,
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_opt(label: impl Into<String>, image: Option<Image>, opt: WidgetOption) -> Self {
Self {
label: label.into(),
image,
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let mut width = padding * 2;
let mut visual_h = 0;
if !self.label.is_empty() {
width += text_size(style, atlas, self.font, &self.label).width;
}
if let Some(Image::Slot(slot)) = self.image {
let size = atlas.get_slot_size(slot);
width = width.max(size.width + padding * 2);
visual_h = size.height;
}
let height = content_height(style, atlas, self.font, visual_h);
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
let rect = ctx.rect();
if control.clicked {
res |= ResourceState::SUBMIT;
}
if !self.opt.has_no_frame() {
if let Some(colorid) = widget_fill_color(control, ControlColor::Button, WidgetFillOption::HOVER | WidgetFillOption::CLICK) {
ctx.draw_frame(rect, colorid);
}
}
if !self.label.is_empty() {
let font = ctx.style().resolve_font_choice(self.font);
ctx.draw_control_text_with_font(font, &self.label, rect, ControlColor::Text, self.opt);
}
if let Some(image) = self.image {
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.push_image(image, rect, color);
}
res
}
}
implement_widget!(ListBox, handle_widget, preferred_size_widget);
#[derive(Clone)]
pub struct Checkbox {
pub label: String,
pub value: bool,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
}
impl Checkbox {
pub fn new(label: impl Into<String>, value: bool) -> Self {
Self {
label: label.into(),
value,
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_opt(label: impl Into<String>, value: bool, opt: WidgetOption) -> Self {
Self {
label: label.into(),
value,
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let check_icon = atlas.get_icon_size(CHECK_ICON);
let height = content_height(style, atlas, self.font, check_icon.height);
let mut width = padding * 2 + height;
if !self.label.is_empty() {
width += text_size(style, atlas, self.font, &self.label).width + padding;
}
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
let bounds = ctx.rect();
let box_rect = rect(bounds.x, bounds.y, bounds.height, bounds.height);
let value = if control.clicked { !self.value } else { self.value };
if control.clicked {
res |= ResourceState::CHANGE;
}
ctx.draw_widget_frame(control, box_rect, ControlColor::Base, self.opt);
if value {
let color = ctx.style().colors[ControlColor::Text as usize];
ctx.draw_icon(CHECK_ICON, box_rect, color);
}
let text_rect = rect(bounds.x + box_rect.width, bounds.y, bounds.width - box_rect.width, bounds.height);
if !self.label.is_empty() {
let font = ctx.style().resolve_font_choice(self.font);
ctx.draw_control_text_with_font(font, &self.label, text_rect, ControlColor::Text, self.opt);
}
res
}
}
impl Widget for Checkbox {
fn widget_opt(&self) -> &WidgetOption {
&self.opt
}
fn behaviour_opt(&self) -> &WidgetBehaviourOption {
&self.bopt
}
fn measure(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni {
self.preferred_size_widget(style, atlas, avail)
}
fn run(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let res = self.handle_widget(ctx, control);
if control.clicked {
self.value = !self.value;
}
res
}
}
#[derive(Clone)]
pub struct Custom {
pub name: String,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
}
impl Custom {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
pub fn with_opt(name: impl Into<String>, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
Self { name: name.into(), opt, bopt }
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let text_w = if self.name.is_empty() {
0
} else {
text_size(style, atlas, FontChoice::default(), self.name.as_str()).width
};
let width = padding * 2 + text_w;
let height = content_height(style, atlas, FontChoice::default(), 0);
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState {
ResourceState::NONE
}
}
implement_widget!(Custom, handle_widget, preferred_size_widget);
#[derive(Clone)]
pub struct Internal {
pub tag: &'static str,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
}
impl Internal {
pub fn new(tag: &'static str) -> Self {
Self {
tag,
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let text_w = if self.tag.is_empty() {
0
} else {
text_size(style, atlas, FontChoice::default(), self.tag).width
};
let width = padding * 2 + text_w;
let height = content_height(style, atlas, FontChoice::default(), 0);
Dimensioni::new(width.max(0), height)
}
fn handle_widget(&mut self, _ctx: &mut WidgetCtx<'_>, _control: &ControlState) -> ResourceState {
ResourceState::NONE
}
}
implement_widget!(Internal, handle_widget, preferred_size_widget);
#[derive(Clone)]
pub struct Combo {
pub popup: WindowHandle,
pub selected: usize,
pub open: bool,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
pub font: FontChoice,
label: String,
clamped: bool,
last_anchor: Recti,
}
impl Combo {
pub fn new(popup: WindowHandle) -> Self {
Self {
popup,
selected: 0,
open: false,
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
font: FontChoice::default(),
label: String::new(),
clamped: false,
last_anchor: Recti::default(),
}
}
pub fn with_opt(popup: WindowHandle, opt: WidgetOption, bopt: WidgetBehaviourOption) -> Self {
Self {
popup,
selected: 0,
open: false,
opt,
bopt,
font: FontChoice::default(),
label: String::new(),
clamped: false,
last_anchor: Recti::default(),
}
}
pub fn anchor(&self) -> Recti {
self.last_anchor
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn close_popup(&mut self) {
self.popup.set_focus(None);
self.popup.close();
self.open = false;
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let padding = style.padding.max(0);
let text_w = if self.label.is_empty() {
0
} else {
text_size(style, atlas, self.font, self.label.as_str()).width
};
let indicator = atlas.get_icon_size(EXPAND_DOWN_ICON);
let width = (padding * 3 + text_w + indicator.width).max(0);
let height = content_height(style, atlas, self.font, indicator.height);
Dimensioni::new(width, height)
}
pub fn update_items<S: AsRef<str>>(&mut self, items: &[S]) {
self.clamped = false;
if items.is_empty() {
if self.selected != 0 {
self.selected = 0;
self.clamped = true;
}
self.label.clear();
return;
}
if self.selected >= items.len() {
self.selected = items.len() - 1;
self.clamped = true;
}
self.label.clear();
if let Some(label) = items.get(self.selected) {
self.label.push_str(label.as_ref());
}
}
pub fn select<S: AsRef<str>>(&mut self, index: usize, items: &[S]) -> Option<String> {
if items.is_empty() {
self.selected = 0;
self.label.clear();
self.close_popup();
return None;
}
self.selected = index.min(items.len() - 1);
self.label.clear();
self.label.push_str(items[self.selected].as_ref());
let selected_label = self.label.clone();
self.close_popup();
Some(selected_label)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
if self.clamped {
res |= ResourceState::CHANGE;
self.clamped = false;
}
if control.clicked {
res |= ResourceState::SUBMIT | ResourceState::ACTIVE;
}
if self.open || self.popup.is_open() {
res |= ResourceState::ACTIVE;
}
let header = ctx.rect();
self.last_anchor = rect(header.x, header.y + header.height, header.width, 1);
ctx.draw_widget_frame(control, header, ControlColor::Button, self.opt);
let indicator_size = ctx.atlas().get_icon_size(EXPAND_DOWN_ICON);
let indicator_x = header.x + header.width - indicator_size.width;
let indicator_y = header.y + ((header.height - indicator_size.height) / 2).max(0);
let indicator = rect(indicator_x, indicator_y, indicator_size.width, indicator_size.height);
let mut text_rect = header;
let reserved_width = indicator_size.width;
text_rect.width = (text_rect.width - reserved_width).max(0);
let font = ctx.style().resolve_font_choice(self.font);
ctx.draw_control_text_with_font(font, self.label.as_str(), text_rect, ControlColor::Text, self.opt);
ctx.draw_widget_frame(control, indicator, ControlColor::Button, self.opt);
let icon_color = ctx.style().colors[ControlColor::Text as usize];
ctx.draw_icon(EXPAND_DOWN_ICON, indicator, icon_color);
res
}
}
impl Widget for Combo {
fn widget_opt(&self) -> &WidgetOption {
&self.opt
}
fn behaviour_opt(&self) -> &WidgetBehaviourOption {
&self.bopt
}
fn measure(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni {
self.preferred_size_widget(style, atlas, avail)
}
fn run(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
if control.clicked {
self.open = !self.open;
if !self.open {
self.close_popup();
}
} else if !self.popup.is_open() {
self.open = false;
}
self.handle_widget(ctx, control)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AtlasSource, CharEntry, FontEntry, Input, SourceFormat};
use std::{cell::RefCell, rc::Rc};
fn make_test_atlas() -> AtlasHandle {
let pixels: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
let icons: Vec<(&str, Recti)> = ["white", "close", "expand", "collapse", "check", "expand_down"]
.iter()
.map(|name| (*name, Recti::new(0, 0, 1, 1)))
.collect();
let entries = vec![(
'a',
CharEntry {
offset: Vec2i::new(0, 0),
advance: Vec2i::new(8, 0),
rect: Recti::new(0, 0, 1, 1),
},
)];
let fonts = vec![(
"default",
FontEntry {
line_size: 10,
baseline: 8,
font_size: 10,
entries: &entries,
},
)];
let source = AtlasSource {
width: 1,
height: 1,
pixels: &pixels,
icons: &icons,
fonts: &fonts,
format: SourceFormat::Raw,
slots: &[],
};
AtlasHandle::from(&source)
}
#[test]
fn combo_run_toggles_open_state() {
let atlas = make_test_atlas();
let style = Rc::new(Style::default());
let input = Rc::new(RefCell::new(Input::default()));
let popup = WindowHandle::popup("combo", atlas.clone(), style.clone(), input);
let mut combo = Combo::new(popup);
let mut commands = Vec::new();
let mut triangle_vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut focus = None;
let mut updated_focus = false;
let rect = rect(0, 0, 100, 20);
let control = ControlState {
hovered: true,
focused: true,
clicked: true,
active: true,
scroll_delta: None,
};
let mut ctx = WidgetCtx::new(
widget_id_of(&combo),
rect,
&mut commands,
&mut triangle_vertices,
&mut clip_stack,
style.as_ref(),
&atlas,
&mut focus,
&mut updated_focus,
true,
None,
);
combo.run(&mut ctx, &control);
assert!(combo.is_open());
combo.popup.open();
let mut ctx = WidgetCtx::new(
widget_id_of(&combo),
rect,
&mut commands,
&mut triangle_vertices,
&mut clip_stack,
style.as_ref(),
&atlas,
&mut focus,
&mut updated_focus,
true,
None,
);
combo.run(&mut ctx, &control);
assert!(!combo.is_open());
assert!(!combo.popup.is_open());
}
#[test]
fn combo_select_updates_label_and_closes_popup() {
let atlas = make_test_atlas();
let style = Rc::new(Style::default());
let input = Rc::new(RefCell::new(Input::default()));
let popup = WindowHandle::popup("combo", atlas, style, input);
let mut combo = Combo::new(popup);
let items = ["Apple", "Banana", "Cherry"];
combo.open = true;
combo.popup.open();
let selected = combo.select(1, &items);
assert_eq!(selected.as_deref(), Some("Banana"));
assert_eq!(combo.selected, 1);
assert!(!combo.is_open());
assert!(!combo.popup.is_open());
}
}