Add support for Button widget, new custom widget AspectRatioContainer

This commit is contained in:
Imbris 2020-05-26 18:26:16 -04:00
parent bc0792a57a
commit 095345d8c6
12 changed files with 500 additions and 46 deletions

View File

@ -158,6 +158,7 @@ rotation_image_ids! {
}
}
#[derive(Clone)] // TODO: why does iced require Clone?
pub enum Event {
LoginAttempt {
username: String,
@ -187,26 +188,59 @@ pub struct PopupData {
// No state currently
struct IcedState {
imgs: IcedImgs,
quit_button: iced::button::State,
}
pub type Message = Event;
impl IcedState {
pub fn new(imgs: IcedImgs) -> Self {
Self {
imgs,
quit_button: iced::button::State::new(),
}
}
pub fn view(&mut self, i18n: &Localization) -> Element<Message> {
use iced::{Align, Column, Container, Length, Row, Space, Text};
// TODO: scale with window size
let button_font_size = 30;
const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
use iced::{
Align, Button, Column, Container, HorizontalAlignment, Length, Row, Space, Text,
VerticalAlignment,
};
use ui::ice::{
compound_graphic::{CompoundGraphic, Graphic},
BackgroundContainer, Image, Padding,
AspectRatioContainer, BackgroundContainer, ButtonStyle, Image, Padding,
};
use vek::*;
let button_style = ButtonStyle::new(self.imgs.button)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.text_color(TEXT_COLOR)
.disabled_text_color(DISABLED_TEXT_COLOR);
let buttons = Column::with_children(vec![
Image::new(self.imgs.button).fix_aspect_ratio().into(),
Image::new(self.imgs.button).fix_aspect_ratio().into(),
/* BackgroundContainer::new(
Image::new(self.imgs.button).fix_aspect_ratio(),
Text::new("Quit"),
AspectRatioContainer::new(
Button::new(
&mut self.quit_button,
Text::new(i18n.get("common.quit"))
.size(button_font_size)
.height(Length::Fill)
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Center)
.vertical_alignment(VerticalAlignment::Center),
)
.height(Length::Fill)
.width(Length::Fill)
.style(button_style)
.on_press(Message::Quit),
)
.into(), */
Text::new(i18n.get("common.quit")).size(40).into(),
.ratio_of_image(self.imgs.button)
.into(),
])
.width(Length::Fill)
.max_width(200)
@ -314,13 +348,7 @@ impl IcedState {
.height(Length::Fill)
.spacing(10);
BackgroundContainer::new(
Image::new(self.imgs.bg)
.width(Length::Fill)
.height(Length::Fill),
content,
)
.into()
BackgroundContainer::new(Image::new(self.imgs.bg), content).into()
}
pub fn update(message: Message) {
@ -413,9 +441,7 @@ impl<'a> MainMenuUi {
};
let mut ice_ui = IcedUi::new(window, ice_font).unwrap();
let ice_state = IcedState {
imgs: IcedImgs::load(&mut ice_ui).expect("Failed to load images"),
};
let ice_state = IcedState::new(IcedImgs::load(&mut ice_ui).expect("Failed to load images"));
Self {
ui,

View File

@ -8,8 +8,9 @@ mod winit_conversion;
pub use cache::Font;
pub use graphic::{Id, Rotation};
pub use iced::Event;
pub use renderer::IcedRenderer;
pub use renderer::{ButtonStyle, IcedRenderer};
pub use widget::{
aspect_ratio_container::AspectRatioContainer,
background_container::{BackgroundContainer, Padding},
compound_graphic,
image::Image,

View File

@ -0,0 +1,25 @@
use super::{
super::widget::{aspect_ratio_container, image},
IcedRenderer,
};
use iced::{Element, Layout, Point, Rectangle};
impl aspect_ratio_container::Renderer for IcedRenderer {
//type Style
type ImageHandle = image::Handle;
fn dimensions(&self, handle: &Self::ImageHandle) -> (u32, u32) { self.image_dims(*handle) }
fn draw<M>(
&mut self,
defaults: &Self::Defaults,
_bounds: Rectangle,
cursor_position: Point,
//style: &Self::Style,
content: &Element<'_, M, Self>,
content_layout: Layout<'_>,
) -> Self::Output {
// TODO: stlying to add a background image and such
content.draw(self, defaults, content_layout, cursor_position)
}
}

View File

@ -0,0 +1,165 @@
use super::{super::Rotation, widget::image, Defaults, IcedRenderer, Primitive};
use iced::{button, mouse, Color, Element, Layout, Point, Rectangle};
use vek::Rgba;
#[derive(Clone, Copy)]
struct Background {
default: image::Handle,
hover: image::Handle,
press: image::Handle,
}
impl Background {
fn new(image: image::Handle) -> Self {
Self {
default: image,
hover: image,
press: image,
}
}
}
// TODO: consider a different place for this
// Note: for now all buttons have an image background
#[derive(Clone, Copy)]
pub struct Style {
background: Option<Background>,
enabled_text: Color,
disabled_text: Color,
/* greying out / changing text color
*disabled: , */
}
impl Style {
pub fn new(image: image::Handle) -> Self {
Self {
background: Some(Background::new(image)),
..Default::default()
}
}
pub fn hover_image(mut self, image: image::Handle) -> Self {
self.background = Some(match self.background {
Some(mut background) => {
background.hover = image;
background
},
None => Background::new(image),
});
self
}
pub fn press_image(mut self, image: image::Handle) -> Self {
self.background = Some(match self.background {
Some(mut background) => {
background.press = image;
background
},
None => Background::new(image),
});
self
}
pub fn text_color(mut self, color: Color) -> Self {
self.enabled_text = color;
self
}
pub fn disabled_text_color(mut self, color: Color) -> Self {
self.disabled_text = color;
self
}
fn disabled(&self) -> (Option<image::Handle>, Color) {
(
self.background.as_ref().map(|b| b.default),
self.disabled_text,
)
}
fn pressed(&self) -> (Option<image::Handle>, Color) {
(self.background.as_ref().map(|b| b.press), self.enabled_text)
}
fn hovered(&self) -> (Option<image::Handle>, Color) {
(self.background.as_ref().map(|b| b.hover), self.enabled_text)
}
fn active(&self) -> (Option<image::Handle>, Color) {
(
self.background.as_ref().map(|b| b.default),
self.enabled_text,
)
}
}
impl Default for Style {
fn default() -> Self {
Self {
background: None,
enabled_text: Color::WHITE,
disabled_text: Color::from_rgb(0.5, 0.5, 0.5),
}
}
}
impl button::Renderer for IcedRenderer {
// TODO: what if this gets large enough to not be copied around?
type Style = Style;
const DEFAULT_PADDING: u16 = 0;
fn draw<M>(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
cursor_position: Point,
is_disabled: bool,
is_pressed: bool,
style: &Self::Style,
content: &Element<'_, M, Self>,
content_layout: Layout<'_>,
) -> Self::Output {
let is_mouse_over = bounds.contains(cursor_position);
let (maybe_image, text_color) = if is_disabled {
style.disabled()
} else if is_mouse_over {
if is_pressed {
style.pressed()
} else {
style.hovered()
}
} else {
style.active()
};
let (content, _) = content.draw(
self,
&Defaults { text_color },
content_layout,
cursor_position,
);
let primitive = if let Some(handle) = maybe_image {
let background = Primitive::Image {
handle: (handle, Rotation::None),
bounds,
color: Rgba::broadcast(255),
};
Primitive::Group {
primitives: vec![background, content],
}
} else {
content
};
let mouse_interaction = if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
};
(primitive, mouse_interaction)
}
}

View File

@ -6,14 +6,7 @@ use iced::mouse;
use vek::Rgba;
impl image::Renderer for IcedRenderer {
fn dimensions(&self, handle: image::Handle) -> (u32, u32) {
self
.cache
.graphic_cache()
.get_graphic_dims((handle, Rotation::None))
// TODO: don't unwrap
.unwrap()
}
fn dimensions(&self, handle: image::Handle) -> (u32, u32) { self.image_dims(handle) }
fn draw(
&mut self,

View File

@ -1,4 +1,7 @@
// TODO: reorganize modules (e.g. put all these in a widget submodule)
mod aspect_ratio_container;
mod background_container;
mod button;
mod column;
mod compound_graphic;
mod container;
@ -7,6 +10,8 @@ mod row;
mod space;
mod text;
pub use button::Style as ButtonStyle;
use super::{
super::graphic::{self, Graphic, TexId},
cache::Cache,
@ -157,6 +162,15 @@ impl IcedRenderer {
self.cache.add_graphic(graphic)
}
fn image_dims(&self, handle: super::widget::image::Handle) -> (u32, u32) {
self
.cache
.graphic_cache()
.get_graphic_dims((handle, Rotation::None))
// TODO: don't unwrap
.unwrap()
}
pub fn resize(&mut self, scaled_dims: Vec2<f32>, renderer: &mut Renderer) {
self.win_dims = scaled_dims;
@ -582,9 +596,22 @@ fn default_scissor(renderer: &Renderer) -> Aabr<u16> {
}
}
// TODO: expose to user
pub struct Defaults {
pub text_color: iced::Color,
}
impl Default for Defaults {
fn default() -> Self {
Self {
text_color: iced::Color::WHITE,
}
}
}
impl iced::Renderer for IcedRenderer {
// Default styling
type Defaults = ();
type Defaults = Defaults;
// TODO: use graph of primitives to enable diffing???
type Output = (Primitive, iced::mouse::Interaction);

View File

@ -28,7 +28,7 @@ impl text::Renderer for IcedRenderer {
fn draw(
&mut self,
_defaults: &Self::Defaults,
defaults: &Self::Defaults,
bounds: Rectangle,
content: &str,
size: u16,
@ -38,25 +38,27 @@ impl text::Renderer for IcedRenderer {
vertical_alignment: VerticalAlignment,
) -> Self::Output {
use glyph_brush::{HorizontalAlign, VerticalAlign};
let h_align = match horizontal_alignment {
HorizontalAlignment::Left => HorizontalAlign::Left,
HorizontalAlignment::Center => HorizontalAlign::Center,
HorizontalAlignment::Right => HorizontalAlign::Right,
// glyph_brush thought it would be a great idea to change what the bounds and
// position mean based on the alignment
// TODO: add option to align based on the geometry of the rendered glyphs
// instead of all possible glyphs
let (x, h_align) = match horizontal_alignment {
HorizontalAlignment::Left => (bounds.x, HorizontalAlign::Left),
HorizontalAlignment::Center => (bounds.center_x(), HorizontalAlign::Center),
HorizontalAlignment::Right => (bounds.x + bounds.width, HorizontalAlign::Right),
};
let v_align = match vertical_alignment {
VerticalAlignment::Top => VerticalAlign::Top,
VerticalAlignment::Center => VerticalAlign::Center,
VerticalAlignment::Bottom => VerticalAlign::Bottom,
let (y, v_align) = match vertical_alignment {
VerticalAlignment::Top => (bounds.y, VerticalAlign::Top),
VerticalAlignment::Center => (bounds.center_y(), VerticalAlign::Center),
VerticalAlignment::Bottom => (bounds.y + bounds.height, VerticalAlign::Bottom),
};
let p_scale = self.p_scale;
let section = glyph_brush::Section {
text: content,
// TODO: do snap to pixel thing here IF it is being done down the line
//screen_position: (bounds.x * p_scale, (self.win_dims.y - bounds.y) * p_scale),
screen_position: (bounds.x * p_scale, bounds.y * p_scale),
screen_position: (x * p_scale, y * p_scale),
bounds: (bounds.width * p_scale, bounds.height * p_scale),
scale: glyph_brush::rusttype::Scale::uniform(size as f32 * p_scale),
layout: glyph_brush::Layout::Wrap {
@ -86,7 +88,7 @@ impl text::Renderer for IcedRenderer {
glyphs,
//size: size as f32,
bounds,
linear_color: color.unwrap_or(Color::BLACK).into_linear().into(),
linear_color: color.unwrap_or(defaults.text_color).into_linear().into(),
/*font,
*horizontal_alignment,
*vertical_alignment, */

View File

@ -1,3 +1,4 @@
pub mod aspect_ratio_container;
pub mod background_container;
pub mod compound_graphic;
pub mod image;

View File

@ -0,0 +1,189 @@
use iced::{layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, Widget};
use std::{hash::Hash, u32};
// Note: it might be more efficient to make this generic over the content type?
enum AspectRatio<I> {
/// Image Id
Image(I),
/// width / height
Ratio(f32),
}
impl<I: Hash> Hash for AspectRatio<I> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Self::Image(i) => i.hash(state),
Self::Ratio(r) => r.to_bits().hash(state),
}
}
}
/// Provides a container that takes on a fixed aspect ratio
/// Thus, can be used to fix the aspect ratio of content if it is set to
/// Length::Fill The aspect ratio may be based on that of an image, in which
/// case the ratio is obtained from the renderer
pub struct AspectRatioContainer<'a, M, R: self::Renderer> {
max_width: u32,
max_height: u32,
aspect_ratio: AspectRatio<R::ImageHandle>,
content: Element<'a, M, R>,
}
impl<'a, M, R> AspectRatioContainer<'a, M, R>
where
R: self::Renderer,
{
pub fn new(content: impl Into<Element<'a, M, R>>) -> Self {
Self {
max_width: u32::MAX,
max_height: u32::MAX,
aspect_ratio: AspectRatio::Ratio(1.0),
content: content.into(),
}
}
/// Set the ratio (width/height)
pub fn ratio(mut self, ratio: f32) -> Self {
self.aspect_ratio = AspectRatio::Ratio(ratio);
self
}
/// Use the ratio of the provided image
pub fn ratio_of_image(mut self, handle: R::ImageHandle) -> Self {
self.aspect_ratio = AspectRatio::Image(handle);
self
}
pub fn max_width(mut self, max_width: u32) -> Self {
self.max_width = max_width;
self
}
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
}
impl<'a, M, R> Widget<M, R> for AspectRatioContainer<'a, M, R>
where
R: self::Renderer,
{
fn width(&self) -> Length { Length::Fill }
fn height(&self) -> Length { Length::Fill }
fn layout(&self, renderer: &R, limits: &layout::Limits) -> layout::Node {
let limits = limits
.loose()
.max_width(self.max_width)
.max_height(self.max_height)
.width(self.width())
.height(self.height());
let aspect_ratio = match &self.aspect_ratio {
AspectRatio::Image(handle) => {
let (pixel_w, pixel_h) = renderer.dimensions(handle);
// Just in case
// could convert to gracefully handling
debug_assert!(pixel_w != 0);
debug_assert!(pixel_h != 0);
pixel_w as f32 / pixel_h as f32
},
AspectRatio::Ratio(ratio) => *ratio,
};
// We need to figure out the max width/height of the limits
// and then adjust one down to meet the aspect ratio
let max_size = limits.max();
let (max_width, max_height) = (max_size.width as f32, max_size.height as f32);
let max_aspect_ratio = max_width / max_height;
let limits = if max_aspect_ratio > aspect_ratio {
limits.max_width((max_height * aspect_ratio) as u32)
} else {
limits.max_height((max_width / aspect_ratio) as u32)
};
let content = self.content.layout(renderer, &limits.loose());
layout::Node::with_children(limits.max(), vec![content])
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<M>,
renderer: &R,
clipboard: Option<&dyn Clipboard>,
) {
self.content.on_event(
event,
layout.children().next().unwrap(),
cursor_position,
messages,
renderer,
clipboard,
);
}
fn draw(
&self,
renderer: &mut R,
defaults: &R::Defaults,
layout: Layout<'_>,
cursor_position: Point,
) -> R::Output {
renderer.draw(
defaults,
layout.bounds(),
cursor_position,
&self.content,
layout.children().next().unwrap(),
)
}
fn hash_layout(&self, state: &mut Hasher) {
struct Marker;
std::any::TypeId::of::<Marker>().hash(state);
self.max_width.hash(state);
self.max_height.hash(state);
self.aspect_ratio.hash(state);
// TODO: add pixel dims (need renderer)
self.content.hash_layout(state);
}
}
pub trait Renderer: iced::Renderer {
/// The style supported by this renderer.
//type Style: Default;
/// The handle used by this renderer for images.
type ImageHandle: Hash;
fn dimensions(&self, handle: &Self::ImageHandle) -> (u32, u32);
fn draw<M>(
&mut self,
defaults: &Self::Defaults,
bounds: Rectangle,
cursor_position: Point,
//style: &Self::Style,
content: &Element<'_, M, Self>,
content_layout: Layout<'_>,
) -> Self::Output;
}
// They got to live ¯\_(ツ)_/¯
impl<'a, M, R> From<AspectRatioContainer<'a, M, R>> for Element<'a, M, R>
where
R: 'a + self::Renderer,
M: 'a,
{
fn from(widget: AspectRatioContainer<'a, M, R>) -> Element<'a, M, R> { Element::new(widget) }
}

View File

@ -1,4 +1,4 @@
use iced::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
use iced::{layout, Clipboard, Element, Event, Hasher, Layout, Length, Point, Size, Widget};
use std::{hash::Hash, u32};
// Note: it might be more efficient to make this generic over the content type
@ -63,7 +63,7 @@ pub trait Background<R: iced::Renderer>: Sized {
fn width(&self) -> Length;
fn height(&self) -> Length;
fn aspect_ratio_fixed(&self) -> bool;
fn pixel_dims(&self, renderer: &R) -> [u16; 2];
fn pixel_dims(&self, renderer: &R) -> (u16, u16);
fn draw(
&self,
renderer: &mut R,
@ -158,7 +158,7 @@ where
.width(self.width())
.height(self.height());
let [pixel_w, pixel_h] = self.background.pixel_dims(renderer);
let (pixel_w, pixel_h) = self.background.pixel_dims(renderer);
let (horizontal_pad_frac, vertical_pad_frac, top_pad_frac, left_pad_frac) = {
let Padding {
top,
@ -208,6 +208,10 @@ where
// again, why is loose() used here?
let mut content = self.content.layout(renderer, &limits.loose());
// TODO: handle cases where self and/or children are not Length::Fill
// If fill use max_size
//if match self.width(), self.height()
// This time we need to adjust up to meet the aspect ratio
// so that the container is larger than the contents
let mut content_size = content.size();
@ -261,6 +265,25 @@ where
layout::Node::with_children(size, vec![content])
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<M>,
renderer: &R,
clipboard: Option<&dyn Clipboard>,
) {
self.content.on_event(
event,
layout.children().next().unwrap(),
cursor_position,
messages,
renderer,
clipboard,
);
}
fn draw(
&self,
renderer: &mut R,

View File

@ -230,7 +230,9 @@ where
fn aspect_ratio_fixed(&self) -> bool { self.fix_aspect_ratio }
fn pixel_dims(&self, _renderer: &R) -> [u16; 2] { self.graphics_size }
fn pixel_dims(&self, _renderer: &R) -> (u16, u16) {
(self.graphics_size[0], self.graphics_size[1])
}
fn draw(
&self,

View File

@ -123,9 +123,9 @@ where
fn aspect_ratio_fixed(&self) -> bool { self.fix_aspect_ratio }
fn pixel_dims(&self, renderer: &R) -> [u16; 2] {
fn pixel_dims(&self, renderer: &R) -> (u16, u16) {
let (w, h) = renderer.dimensions(self.handle);
[w as u16, h as u16]
(w as u16, h as u16)
}
fn draw(