use crate::style::Style;
use crate::tree::NodeId;
use crate::widgets::Widget;
use astrelis_core::math::Vec2;
use astrelis_render::Color;
use astrelis_text::{FontRenderer, FontWeight, Text as TextStyle, TextAlign, VerticalAlign};
use std::any::Any;
use std::rc::Rc;
use std::sync::Arc;
fn darken(color: Color, amount: f32) -> Color {
let factor = 1.0 - amount;
Color::rgba(
color.r * factor,
color.g * factor,
color.b * factor,
color.a,
)
}
fn lighten(color: Color, amount: f32) -> Color {
Color::rgba(
color.r + (1.0 - color.r) * amount,
color.g + (1.0 - color.g) * amount,
color.b + (1.0 - color.b) * amount,
color.a,
)
}
pub type ImageTexture = Arc<astrelis_render::wgpu::TextureView>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ImageUV {
pub u_min: f32,
pub v_min: f32,
pub u_max: f32,
pub v_max: f32,
}
impl Default for ImageUV {
fn default() -> Self {
Self {
u_min: 0.0,
v_min: 0.0,
u_max: 1.0,
v_max: 1.0,
}
}
}
impl ImageUV {
pub fn full() -> Self {
Self::default()
}
pub fn from_sprite(
sprite_x: u32,
sprite_y: u32,
sprite_width: u32,
sprite_height: u32,
texture_width: u32,
texture_height: u32,
) -> Self {
Self {
u_min: sprite_x as f32 / texture_width as f32,
v_min: sprite_y as f32 / texture_height as f32,
u_max: (sprite_x + sprite_width) as f32 / texture_width as f32,
v_max: (sprite_y + sprite_height) as f32 / texture_height as f32,
}
}
pub fn new(u_min: f32, v_min: f32, u_max: f32, v_max: f32) -> Self {
Self {
u_min,
v_min,
u_max,
v_max,
}
}
pub fn flip_h(self) -> Self {
Self {
u_min: self.u_max,
u_max: self.u_min,
..self
}
}
pub fn flip_v(self) -> Self {
Self {
v_min: self.v_max,
v_max: self.v_min,
..self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ImageFit {
Fill,
#[default]
Contain,
Cover,
None,
}
#[derive(Clone)]
pub struct Image {
pub texture: Option<ImageTexture>,
pub uv: ImageUV,
pub tint: Color,
pub fit: ImageFit,
pub natural_width: f32,
pub natural_height: f32,
pub border_radius: f32,
pub sampling: astrelis_render::ImageSampling,
pub style: Style,
}
impl Image {
pub fn new() -> Self {
Self {
texture: None,
uv: ImageUV::default(),
tint: Color::WHITE,
fit: ImageFit::default(),
natural_width: 0.0,
natural_height: 0.0,
border_radius: 0.0,
sampling: astrelis_render::ImageSampling::default(),
style: Style::new(),
}
}
pub fn with_texture(texture: ImageTexture, width: f32, height: f32) -> Self {
Self {
texture: Some(texture),
uv: ImageUV::default(),
tint: Color::WHITE,
fit: ImageFit::default(),
natural_width: width,
natural_height: height,
border_radius: 0.0,
sampling: astrelis_render::ImageSampling::default(),
style: Style::new().width(width).height(height),
}
}
pub fn texture(mut self, texture: ImageTexture) -> Self {
self.texture = Some(texture);
self
}
pub fn uv(mut self, uv: ImageUV) -> Self {
self.uv = uv;
self
}
pub fn tint(mut self, color: Color) -> Self {
self.tint = color;
self
}
pub fn fit(mut self, fit: ImageFit) -> Self {
self.fit = fit;
self
}
pub fn natural_size(mut self, width: f32, height: f32) -> Self {
self.natural_width = width;
self.natural_height = height;
self
}
pub fn border_radius(mut self, radius: f32) -> Self {
self.border_radius = radius;
self
}
pub fn sampling(mut self, sampling: astrelis_render::ImageSampling) -> Self {
self.sampling = sampling;
self
}
pub fn set_texture(&mut self, texture: ImageTexture) {
self.texture = Some(texture);
}
pub fn set_uv(&mut self, uv: ImageUV) {
self.uv = uv;
}
pub fn set_tint(&mut self, color: Color) {
self.tint = color;
}
}
impl Default for Image {
fn default() -> Self {
Self::new()
}
}
impl Widget for Image {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn measure(&self, _available_space: Vec2, _font_renderer: Option<&FontRenderer>) -> Vec2 {
Vec2::new(self.natural_width, self.natural_height)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct Container {
pub style: Style,
pub children: Vec<NodeId>,
}
impl Container {
pub fn new() -> Self {
Self {
style: Style::new().display(taffy::Display::Flex),
children: Vec::new(),
}
}
pub fn with_style(style: Style) -> Self {
Self {
style,
children: Vec::new(),
}
}
}
impl Default for Container {
fn default() -> Self {
Self::new()
}
}
impl Widget for Container {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn children(&self) -> &[NodeId] {
&self.children
}
fn children_mut(&mut self) -> Option<&mut Vec<NodeId>> {
Some(&mut self.children)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct Text {
pub content: String,
pub font_size: f32,
pub color: Option<Color>,
pub weight: FontWeight,
pub align: TextAlign,
pub vertical_align: VerticalAlign,
pub style: Style,
pub font_id: u32,
pub wrap_width_constraint: Option<crate::constraint::Constraint>,
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
font_size: 16.0,
color: None,
weight: FontWeight::Normal,
align: TextAlign::Left,
vertical_align: VerticalAlign::Top,
style: Style::new(),
font_id: 0,
wrap_width_constraint: None,
}
}
pub fn font_id(mut self, font_id: u32) -> Self {
self.font_id = font_id;
self
}
pub fn size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.weight = weight;
self
}
pub fn bold(mut self) -> Self {
self.weight = FontWeight::Bold;
self
}
pub fn align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub fn vertical_align(mut self, vertical_align: VerticalAlign) -> Self {
self.vertical_align = vertical_align;
self
}
pub fn max_wrap_width(mut self, width: impl Into<crate::constraint::Constraint>) -> Self {
self.wrap_width_constraint = Some(width.into());
self
}
pub fn build_text_style(&self) -> TextStyle {
self.build_text_style_with_viewport(None)
}
pub fn resolve_color(&self, default: Color) -> Color {
self.color.unwrap_or(default)
}
pub fn build_text_style_with_viewport(&self, viewport: Option<Vec2>) -> TextStyle {
use crate::constraint_resolver::{ConstraintResolver, ResolveContext};
let color = self.color.unwrap_or(Color::WHITE);
let mut text = TextStyle::new(&self.content)
.size(self.font_size)
.color(color)
.weight(self.weight)
.align(self.align)
.vertical_align(self.vertical_align);
if let Some(ref constraint) = self.wrap_width_constraint {
if let Some(vp) = viewport {
let ctx = ResolveContext::viewport_only(vp);
if let Some(width) = ConstraintResolver::resolve(constraint, &ctx) {
text = text.max_width(width);
return text;
}
}
if let Some(taffy::Dimension::Length(width)) = constraint.try_to_dimension() {
text = text.max_width(width);
return text;
}
}
if let taffy::Dimension::Length(width) = self.style.layout.size.width {
text = text.max_width(width);
}
text
}
pub fn set_content(&mut self, content: impl Into<String>) -> bool {
let new_content = content.into();
if self.content != new_content {
self.content = new_content;
true
} else {
false
}
}
pub fn get_content(&self) -> &str {
&self.content
}
pub fn set_font_size(&mut self, size: f32) {
self.font_size = size;
}
pub fn set_color(&mut self, color: Color) {
self.color = Some(color);
}
}
impl Widget for Text {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn measure(&self, _available_space: Vec2, font_renderer: Option<&FontRenderer>) -> Vec2 {
if let Some(renderer) = font_renderer {
let text_style = self.build_text_style();
let (width, height) = renderer.measure_text(&text_style);
return Vec2::new(width, height);
}
let char_count = self.content.chars().count() as f32;
let estimated_width = char_count * self.font_size * 0.6;
let estimated_height = self.font_size * 1.2;
Vec2::new(estimated_width, estimated_height)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
pub type ButtonCallback = Rc<dyn Fn()>;
#[derive(Clone)]
pub struct Button {
pub label: String,
pub style: Style,
pub hover_color: Option<Color>,
pub active_color: Option<Color>,
pub text_color: Option<Color>,
pub font_size: f32,
pub is_hovered: bool,
pub is_pressed: bool,
pub on_click: Option<ButtonCallback>,
pub font_id: u32,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
style: Style::new()
.display(taffy::Display::Flex)
.padding(10.0)
.border_radius(4.0),
hover_color: None,
active_color: None,
text_color: None,
font_size: 16.0,
is_hovered: false,
is_pressed: false,
on_click: None,
font_id: 0,
}
}
pub fn font_id(mut self, font_id: u32) -> Self {
self.font_id = font_id;
self
}
pub fn on_click<F>(mut self, callback: F) -> Self
where
F: Fn() + 'static,
{
self.on_click = Some(Rc::new(callback));
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.style = self.style.background_color(color);
self
}
pub fn hover_color(mut self, color: Color) -> Self {
self.hover_color = Some(color);
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn current_bg_color(&self) -> Color {
let base = self
.style
.background_color
.unwrap_or(Color::from_rgb_u8(130, 110, 245));
if self.is_pressed {
self.active_color.unwrap_or_else(|| darken(base, 0.15))
} else if self.is_hovered {
self.hover_color.unwrap_or_else(|| lighten(base, 0.1))
} else {
base
}
}
pub fn current_bg_color_themed(
&self,
primary: Color,
hover: Option<Color>,
active: Option<Color>,
) -> Color {
let base = self.style.background_color.unwrap_or(primary);
if self.is_pressed {
self.active_color
.or(active)
.unwrap_or_else(|| darken(base, 0.15))
} else if self.is_hovered {
self.hover_color
.or(hover)
.unwrap_or_else(|| lighten(base, 0.1))
} else {
base
}
}
pub fn set_label(&mut self, label: impl Into<String>) -> bool {
let new_label = label.into();
if self.label != new_label {
self.label = new_label;
true
} else {
false
}
}
pub fn get_label(&self) -> &str {
&self.label
}
pub fn set_hover_color(&mut self, color: Color) {
self.hover_color = Some(color);
}
pub fn set_text_color(&mut self, color: Color) {
self.text_color = Some(color);
}
}
impl Widget for Button {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn measure(&self, _available_space: Vec2, font_renderer: Option<&FontRenderer>) -> Vec2 {
if let Some(renderer) = font_renderer {
let text_style = TextStyle::new(&self.label)
.size(self.font_size)
.color(self.text_color.unwrap_or(Color::WHITE));
let (text_width, text_height) = renderer.measure_text(&text_style);
let padding_x = match self.style.layout.padding.left {
taffy::LengthPercentage::Length(l) => l,
_ => 0.0,
} + match self.style.layout.padding.right {
taffy::LengthPercentage::Length(r) => r,
_ => 0.0,
};
let padding_y = match self.style.layout.padding.top {
taffy::LengthPercentage::Length(t) => t,
_ => 0.0,
} + match self.style.layout.padding.bottom {
taffy::LengthPercentage::Length(b) => b,
_ => 0.0,
};
return Vec2::new(text_width + padding_x, text_height + padding_y);
}
let char_count = self.label.chars().count() as f32;
let estimated_width = char_count * self.font_size * 0.6 + 20.0;
let estimated_height = self.font_size * 1.2 + 20.0;
Vec2::new(estimated_width, estimated_height)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct Row {
pub style: Style,
pub children: Vec<NodeId>,
}
impl Row {
pub fn new() -> Self {
Self {
style: Style::new()
.display(taffy::Display::Flex)
.flex_direction(taffy::FlexDirection::Row),
children: Vec::new(),
}
}
pub fn gap(mut self, gap: impl Into<crate::constraint::Constraint> + Copy) -> Self {
self.style = self.style.gap(gap);
self
}
}
impl Default for Row {
fn default() -> Self {
Self::new()
}
}
impl Widget for Row {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn children(&self) -> &[NodeId] {
&self.children
}
fn children_mut(&mut self) -> Option<&mut Vec<NodeId>> {
Some(&mut self.children)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct TextInput {
pub content: String,
pub placeholder: String,
pub font_size: f32,
pub text_color: Option<Color>,
pub placeholder_color: Option<Color>,
pub style: Style,
pub is_focused: bool,
pub cursor_position: usize,
pub max_length: Option<usize>,
pub on_change: Option<Rc<dyn Fn(String)>>,
}
impl TextInput {
pub fn new(placeholder: impl Into<String>) -> Self {
Self {
content: String::new(),
placeholder: placeholder.into(),
font_size: 16.0,
text_color: None,
placeholder_color: None,
style: Style::new()
.display(taffy::Display::Flex)
.padding(10.0)
.border_width(1.0)
.border_radius(4.0),
is_focused: false,
cursor_position: 0,
max_length: None,
on_change: None,
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
let content_str = content.into();
self.cursor_position = content_str.len();
self.content = content_str;
self
}
pub fn set_value(&mut self, value: impl Into<String>) -> bool {
let value_str = value.into();
self.cursor_position = value_str.len();
if self.content != value_str {
self.content = value_str;
true
} else {
false
}
}
pub fn get_value(&self) -> &str {
&self.content
}
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
self.placeholder = placeholder.into();
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn placeholder_color(mut self, color: Color) -> Self {
self.placeholder_color = Some(color);
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(String) + 'static,
{
self.on_change = Some(Rc::new(callback));
self
}
pub fn insert_char(&mut self, c: char) {
if let Some(max) = self.max_length
&& self.content.len() >= max
{
return;
}
self.content.insert(self.cursor_position, c);
self.cursor_position += 1;
if let Some(ref callback) = self.on_change {
callback(self.content.clone());
}
}
pub fn delete_char(&mut self) {
if self.cursor_position > 0 {
self.cursor_position -= 1;
self.content.remove(self.cursor_position);
if let Some(ref callback) = self.on_change {
callback(self.content.clone());
}
}
}
pub fn display_text(&self) -> &str {
if self.content.is_empty() {
&self.placeholder
} else {
&self.content
}
}
pub fn display_color(&self) -> Color {
if self.content.is_empty() {
self.placeholder_color
.unwrap_or(Color::from_rgb_u8(120, 120, 120))
} else {
self.text_color.unwrap_or(Color::WHITE)
}
}
}
impl Widget for TextInput {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn measure(&self, _available_space: Vec2, font_renderer: Option<&FontRenderer>) -> Vec2 {
if let Some(renderer) = font_renderer {
let text = if self.content.is_empty() {
&self.placeholder
} else {
&self.content
};
let text_style = TextStyle::new(text)
.size(self.font_size)
.color(self.display_color());
let (text_width, text_height) = renderer.measure_text(&text_style);
let padding_x = match self.style.layout.padding.left {
taffy::LengthPercentage::Length(l) => l,
_ => 0.0,
} + match self.style.layout.padding.right {
taffy::LengthPercentage::Length(r) => r,
_ => 0.0,
};
let padding_y = match self.style.layout.padding.top {
taffy::LengthPercentage::Length(t) => t,
_ => 0.0,
} + match self.style.layout.padding.bottom {
taffy::LengthPercentage::Length(b) => b,
_ => 0.0,
};
return Vec2::new(text_width + padding_x + 20.0, text_height + padding_y);
}
Vec2::new(200.0, 40.0)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub struct Tooltip {
pub text: String,
pub style: Style,
pub font_size: f32,
pub text_color: Option<Color>,
pub visible: bool,
}
impl Tooltip {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
style: Style::new()
.display(taffy::Display::Flex)
.padding(8.0)
.border_width(1.0)
.border_radius(4.0)
.position(taffy::Position::Absolute),
font_size: 12.0,
text_color: None,
visible: false,
}
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn background_color(mut self, color: Color) -> Self {
self.style = self.style.background_color(color);
self
}
}
impl Widget for Tooltip {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn style(&self) -> &Style {
&self.style
}
fn style_mut(&mut self) -> &mut Style {
&mut self.style
}
fn measure(&self, _available_space: Vec2, font_renderer: Option<&FontRenderer>) -> Vec2 {
if let Some(renderer) = font_renderer {
let text_style = TextStyle::new(&self.text)
.size(self.font_size)
.color(self.text_color.unwrap_or(Color::WHITE));
let (text_width, text_height) = renderer.measure_text(&text_style);
let padding_x = match self.style.layout.padding.left {
taffy::LengthPercentage::Length(l) => l,
_ => 0.0,
} + match self.style.layout.padding.right {
taffy::LengthPercentage::Length(r) => r,
_ => 0.0,
};
let padding_y = match self.style.layout.padding.top {
taffy::LengthPercentage::Length(t) => t,
_ => 0.0,
} + match self.style.layout.padding.bottom {
taffy::LengthPercentage::Length(b) => b,
_ => 0.0,
};
return Vec2::new(text_width + padding_x, text_height + padding_y);
}
Vec2::new(100.0, 30.0)
}
fn clone_box(&self) -> Box<dyn Widget> {
Box::new(self.clone())
}
}