Add Scrollable widget support, implement disclaimer screen, rearrangements of main menu ui code

This commit is contained in:
Imbris 2020-06-05 02:21:23 -04:00
parent 01eb061a83
commit e2bf974ecb
13 changed files with 450 additions and 131 deletions

View File

@ -255,7 +255,7 @@ impl PlayState for MainMenuState {
}, },
MainMenuEvent::Settings => {}, // TODO MainMenuEvent::Settings => {}, // TODO
MainMenuEvent::Quit => return PlayStateResult::Shutdown, MainMenuEvent::Quit => return PlayStateResult::Shutdown,
/*MainMenuEvent::DisclaimerClosed => { /*MainMenuEvent::DisclaimerAccepted => {
global_state.settings.show_disclaimer = false global_state.settings.show_disclaimer = false
},*/ },*/
MainMenuEvent::AuthServerTrust(auth_server, trust) => { MainMenuEvent::AuthServerTrust(auth_server, trust) => {

View File

@ -3,12 +3,10 @@ use crate::{
i18n::Localization, i18n::Localization,
ui::{ ui::{
fonts::IcedFonts as Fonts, fonts::IcedFonts as Fonts,
ice::{component::neat_button, style, widget::image, Element}, ice::{component::neat_button, style, Element},
}, },
}; };
use iced::{ use iced::{button, Align, Color, Column, Container, Length, Row, Space, Text, VerticalAlignment};
button, Align, Color, Column, Container, HorizontalAlignment, Length, Row, Space, Text,
};
/// Connecting screen for the main menu /// Connecting screen for the main menu
pub struct Screen { pub struct Screen {
@ -16,10 +14,6 @@ pub struct Screen {
add_button: button::State, add_button: button::State,
} }
// TODO: move to super and unify with identical login consts
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);
impl Screen { impl Screen {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -32,32 +26,22 @@ impl Screen {
&mut self, &mut self,
fonts: &Fonts, fonts: &Fonts,
imgs: &Imgs, imgs: &Imgs,
bg_img: image::Handle,
connection_state: &ConnectionState, connection_state: &ConnectionState,
version: &str,
time: f32, time: f32,
i18n: &Localization, i18n: &Localization,
button_style: style::button::Style,
) -> Element<Message> { ) -> Element<Message> {
let fade_msg = (time * 2.0).sin() * 0.5 + 0.51; let fade_msg = (time * 2.0).sin() * 0.5 + 0.51;
let button_style = style::button::Style::new(imgs.button)
.hover_image(imgs.button_hover)
.press_image(imgs.button_press)
.text_color(TEXT_COLOR)
.disabled_text_color(DISABLED_TEXT_COLOR);
let version = Text::new(version) let children = match connection_state {
.size(fonts.cyri.scale(15)) // move version text size to const
.width(Length::Fill)
.height(if matches!(connection_state, ConnectionState::InProgress {..}){Length::Fill}else{Length::Shrink})
.horizontal_alignment(HorizontalAlignment::Right);
let (middle, bottom) = match connection_state {
ConnectionState::InProgress { status } => { ConnectionState::InProgress { status } => {
let status = Text::new(status) let status = Text::new(status)
.size(fonts.alkhemi.scale(80)) .size(fonts.alkhemi.scale(80))
.font(fonts.alkhemi.id) .font(fonts.alkhemi.id)
.color(Color::from_rgba(1.0, 1.0, 1.0, fade_msg)) .color(Color::from_rgba(1.0, 1.0, 1.0, fade_msg))
.width(Length::Fill); .vertical_alignment(VerticalAlignment::Bottom)
.width(Length::Fill)
.height(Length::Fill);
let status = Row::with_children(vec![ let status = Row::with_children(vec![
Space::new(Length::Units(80), Length::Shrink).into(), Space::new(Length::Units(80), Length::Shrink).into(),
@ -78,7 +62,7 @@ impl Screen {
.center_x() .center_x()
.padding(3); .padding(3);
(status.into(), cancel.into()) vec![status.into(), cancel.into()]
}, },
ConnectionState::AuthTrustPrompt { msg, .. } => { ConnectionState::AuthTrustPrompt { msg, .. } => {
let text = Text::new(msg).size(fonts.cyri.scale(25)); let text = Text::new(msg).size(fonts.cyri.scale(25));
@ -125,22 +109,16 @@ impl Screen {
.center_x() .center_x()
.center_y(); .center_y();
( vec![
container.into(), container.into(),
Space::new(Length::Fill, Length::Units(fonts.cyri.scale(15))).into(), Space::new(Length::Fill, Length::Units(fonts.cyri.scale(15))).into(),
) ]
}, },
}; };
let content = Column::with_children(vec![version.into(), middle, bottom]) Column::with_children(children)
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(3);
// Note: could replace this with styling on iced's container since we aren't
// using fixed aspect ratio
Container::new(content)
.style(style::container::Style::image(bg_img))
.into() .into()
} }
} }

View File

@ -0,0 +1,78 @@
use super::{IcedImgs as Imgs, Message};
use crate::{
i18n::Localization,
ui::{
fonts::IcedFonts as Fonts,
ice::{component::neat_button, style, Element},
},
};
use iced::{button, scrollable, Column, Container, Length, Scrollable, Space};
use vek::Rgba;
/// Connecting screen for the main menu
pub struct Screen {
accept_button: button::State,
scroll: scrollable::State,
}
impl Screen {
pub fn new() -> Self {
Self {
accept_button: Default::default(),
scroll: Default::default(),
}
}
pub(super) fn view(
&mut self,
fonts: &Fonts,
imgs: &Imgs,
i18n: &Localization,
button_style: style::button::Style,
) -> Element<Message> {
Container::new(
Container::new(
Column::with_children(vec![
iced::Text::new(i18n.get("common.disclaimer"))
.font(fonts.alkhemi.id)
.size(fonts.alkhemi.scale(35))
.into(),
Space::new(Length::Fill, Length::Units(20)).into(),
Scrollable::new(&mut self.scroll)
.push(
iced::Text::new(i18n.get("main.notice"))
.font(fonts.cyri.id)
.size(fonts.cyri.scale(23)),
)
.height(Length::FillPortion(1))
.into(),
Container::new(
Container::new(neat_button(
&mut self.accept_button,
i18n.get("common.accept"),
0.7,
button_style,
Some(Message::AcceptDisclaimer),
))
.height(Length::Units(fonts.cyri.scale(50))),
)
.center_x()
.height(Length::Shrink)
.width(Length::Fill)
.into(),
])
.spacing(5)
.padding(20)
.width(Length::Fill)
.height(Length::Fill),
)
.style(style::container::Style::Color(Rgba::new(22, 19, 17, 255))),
)
.center_x()
.center_y()
.padding(70)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}

View File

@ -1,4 +1,4 @@
use super::{IcedImgs as Imgs, Info, LoginInfo, Message}; use super::{IcedImgs as Imgs, LoginInfo, Message};
use crate::{ use crate::{
i18n::Localization, i18n::Localization,
ui::{ ui::{
@ -14,14 +14,11 @@ use crate::{
}, },
}, },
}; };
use iced::{ use iced::{button, text_input, Align, Column, Container, Length, Row, Space, Text, TextInput};
button, text_input, Align, Column, Container, HorizontalAlignment, Length, Row, Space, Text,
TextInput,
};
use vek::*; use vek::*;
const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0); pub 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); pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
const FILL_FRAC_ONE: f32 = 0.77; const FILL_FRAC_ONE: f32 = 0.77;
const FILL_FRAC_TWO: f32 = 0.53; const FILL_FRAC_TWO: f32 = 0.53;
const INPUT_WIDTH: u16 = 250; const INPUT_WIDTH: u16 = 250;
@ -52,18 +49,11 @@ impl Screen {
fonts: &Fonts, fonts: &Fonts,
imgs: &Imgs, imgs: &Imgs,
login_info: &LoginInfo, login_info: &LoginInfo,
info: &Info,
error: Option<&str>, error: Option<&str>,
version: &str,
show_servers: bool, show_servers: bool,
i18n: &Localization, i18n: &Localization,
button_style: style::button::Style,
) -> Element<Message> { ) -> Element<Message> {
let button_style = style::button::Style::new(imgs.button)
.hover_image(imgs.button_hover)
.press_image(imgs.button_press)
.text_color(TEXT_COLOR)
.disabled_text_color(DISABLED_TEXT_COLOR);
let buttons = Column::with_children(vec![ let buttons = Column::with_children(vec![
neat_button( neat_button(
&mut self.servers_button, &mut self.servers_button,
@ -96,32 +86,28 @@ impl Screen {
.height(Length::Fill) .height(Length::Fill)
.align_y(Align::End); .align_y(Align::End);
let left_column = if matches!(info, Info::Intro) { let intro_text = i18n.get("main.login_process");
let intro_text = i18n.get("main.login_process");
let info_window = BackgroundContainer::new( let info_window = BackgroundContainer::new(
CompoundGraphic::from_graphics(vec![ CompoundGraphic::from_graphics(vec![
Graphic::rect(Rgba::new(0, 0, 0, 240), [500, 300], [0, 0]), Graphic::rect(Rgba::new(0, 0, 0, 240), [500, 300], [0, 0]),
// Note: a way to tell it to keep the height of this one piece constant and // Note: a way to tell it to keep the height of this one piece constant and
// unstreched would be nice, I suppose we could just break this out into a // unstreched would be nice, I suppose we could just break this out into a
// column and use Length::Units // column and use Length::Units
Graphic::image(imgs.banner_bottom, [500, 30], [0, 300]) Graphic::image(imgs.banner_bottom, [500, 30], [0, 300])
.color(Rgba::new(255, 255, 255, 240)), .color(Rgba::new(255, 255, 255, 240)),
]) ])
.height(Length::Shrink), .height(Length::Shrink),
Text::new(intro_text).size(fonts.cyri.scale(21)), Text::new(intro_text).size(fonts.cyri.scale(21)),
) )
.max_width(450) .max_width(450)
.padding(Padding::new().horizontal(20).top(10).bottom(60)); .padding(Padding::new().horizontal(20).top(10).bottom(60));
Column::with_children(vec![info_window.into(), buttons.into()]) let left_column = Column::with_children(vec![info_window.into(), buttons.into()])
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.padding(27) .padding(27)
.into() .into();
} else {
buttons.into()
};
let banner = self let banner = self
.banner .banner
@ -133,12 +119,9 @@ impl Screen {
.align_x(Align::Center) .align_x(Align::Center)
.align_y(Align::Center); .align_y(Align::Center);
let right_column = Text::new(version) let right_column = Space::new(Length::Fill, Length::Fill);
.size(fonts.cyri.scale(15))
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Right);
let content = Row::with_children(vec![ Row::with_children(vec![
left_column, left_column,
central_column.into(), central_column.into(),
right_column.into(), right_column.into(),
@ -146,11 +129,7 @@ impl Screen {
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.spacing(10) .spacing(10)
.padding(3); .into()
Container::new(content)
.style(style::container::Style::image(imgs.bg))
.into()
} }
} }

View File

@ -1,4 +1,5 @@
mod connecting; mod connecting;
//mod disclaimer;
mod login; mod login;
use crate::{ use crate::{
@ -34,7 +35,8 @@ const COL1: Color = Color::Rgba(0.07, 0.1, 0.1, 0.9);
/*const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue /*const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);*/ const UI_HIGHLIGHT_0: Color = Color::Rgba(0.79, 1.09, 1.09, 1.0);*/
use iced::text_input; use iced::{text_input, Column, Container, HorizontalAlignment, Length};
use ui::ice::style;
image_ids_ice! { image_ids_ice! {
struct IcedImgs { struct IcedImgs {
<VoxelGraphic> <VoxelGraphic>
@ -94,6 +96,7 @@ pub enum Event {
Quit, Quit,
Settings, Settings,
//DisclaimerClosed, TODO: remove all traces? //DisclaimerClosed, TODO: remove all traces?
//DisclaimerAccepted,
AuthServerTrust(String, bool), AuthServerTrust(String, bool),
} }
@ -114,11 +117,6 @@ pub struct LoginInfo {
pub server: String, pub server: String,
} }
enum Info {
//Disclaimer,
Intro,
}
enum ConnectionState { enum ConnectionState {
InProgress { InProgress {
status: String, status: String,
@ -140,6 +138,9 @@ impl ConnectionState {
} }
enum Screen { enum Screen {
/*Disclaimer {
screen: disclaimer::Screen,
},*/
Login { Login {
screen: login::Screen, screen: login::Screen,
// Error to display in a box // Error to display in a box
@ -162,7 +163,6 @@ struct IcedState {
login_info: LoginInfo, login_info: LoginInfo,
show_servers: bool, show_servers: bool,
info: Info,
time: f32, time: f32,
screen: Screen, screen: Screen,
@ -183,7 +183,8 @@ enum Message {
TrustPromptAdd, TrustPromptAdd,
TrustPromptCancel, TrustPromptCancel,
CloseError, CloseError,
//CloseDisclaimer, /*CloseDisclaimer,
*AcceptDisclaimer, */
} }
impl IcedState { impl IcedState {
@ -200,10 +201,15 @@ impl IcedState {
common::util::GIT_VERSION.to_string() common::util::GIT_VERSION.to_string()
); );
let info = Info::Intro; // if settings.show_disclaimer { let screen = /* if settings.show_disclaimer {
//Info::Disclaimer Screen::Disclaimer {
//} else { screen: disclaimer::Screen::new(),
//Info::Intro }
} else { */
Screen::Login {
screen: login::Screen::new(),
error: None,
};
//}; //};
Self { Self {
@ -220,30 +226,47 @@ impl IcedState {
}, },
show_servers: false, show_servers: false,
info,
time: 0.0, time: 0.0,
screen: Screen::Login { screen,
screen: login::Screen::new(),
error: None,
},
} }
} }
fn view(&mut self, dt: f32) -> Element<Message> { fn view(&mut self, dt: f32) -> Element<Message> {
self.time = self.time + dt; self.time = self.time + dt;
// TODO: make disclaimer it's own screen? // TODO: consider setting this as the default in the renderer
match &mut self.screen { let button_style = style::button::Style::new(self.imgs.button)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.text_color(login::TEXT_COLOR)
.disabled_text_color(login::DISABLED_TEXT_COLOR);
let version = iced::Text::new(&self.version)
.size(self.fonts.cyri.scale(15))
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Right);
let bg_img = if matches!(&self.screen, Screen::Connecting {..}) {
self.bg_img
} else {
self.imgs.bg
};
// TODO: make any large text blocks scrollable so that if the area is to
// small they can still be read
let content = match &mut self.screen {
/*Screen::Disclaimer { screen } => {
screen.view(&self.fonts, &self.imgs, &self.i18n, button_style)
},*/
Screen::Login { screen, error } => screen.view( Screen::Login { screen, error } => screen.view(
&self.fonts, &self.fonts,
&self.imgs, &self.imgs,
&self.login_info, &self.login_info,
&self.info,
error.as_deref(), error.as_deref(),
&self.version,
self.show_servers, self.show_servers,
&self.i18n, &self.i18n,
button_style,
), ),
Screen::Connecting { Screen::Connecting {
screen, screen,
@ -251,13 +274,22 @@ impl IcedState {
} => screen.view( } => screen.view(
&self.fonts, &self.fonts,
&self.imgs, &self.imgs,
self.bg_img,
&connection_state, &connection_state,
&self.version,
self.time, self.time,
&self.i18n, &self.i18n,
button_style,
), ),
} };
Container::new(
Column::with_children(vec![version.into(), content])
.spacing(3)
.width(Length::Fill)
.height(Length::Fill),
)
.style(style::container::Style::image(bg_img))
.padding(3)
.into()
} }
fn update(&mut self, message: Message, events: &mut Vec<Event>) { fn update(&mut self, message: Message, events: &mut Vec<Event>) {
@ -330,6 +362,15 @@ impl IcedState {
//Message::CloseDisclaimer => { //Message::CloseDisclaimer => {
// events.push(Event::DisclaimerClosed); // events.push(Event::DisclaimerClosed);
//}, //},
/*Message::AcceptDisclaimer => {
if let Screen::Disclaimer { .. } = &self.screen {
events.push(Event::DisclaimerAccepted);
self.screen = Screen::Login {
screen: login::Screen::new(),
error: None,
};
}
},*/
} }
} }
@ -816,7 +857,7 @@ impl<'a> MainMenuUi {
.was_clicked() .was_clicked()
{ {
self.show_disclaimer = false; self.show_disclaimer = false;
events.push(Event::DisclaimerClosed); events.push(Event::DisclaimerAccepted);
} }
} else {*/ } else {*/
// TODO: Don't use macros for this? // TODO: Don't use macros for this?

View File

@ -63,6 +63,8 @@ impl IcedUi {
use iced::window; use iced::window;
match event { match event {
// Intercept resizing events // Intercept resizing events
// TODO: examine if we are handling dpi properly here
// ideally these values should be the logical ones
Event::Window(window::Event::Resized { width, height }) => { Event::Window(window::Event::Resized { width, height }) => {
self.window_resized = Some(Vec2::new(width, height)); self.window_resized = Some(Vec2::new(width, height));
}, },

View File

@ -86,7 +86,7 @@ pub struct IcedRenderer {
// Per-frame/update // Per-frame/update
current_state: State, current_state: State,
mesh: Mesh<UiPipeline>, mesh: Mesh<UiPipeline>,
glyphs: Vec<(usize, usize, Rgba<f32>)>, glyphs: Vec<(usize, usize, Rgba<f32>, Vec2<u32>)>,
// Output from glyph_brush in the previous frame // Output from glyph_brush in the previous frame
// It can sometimes ask you to redraw with these instead (idk if that is done with // It can sometimes ask you to redraw with these instead (idk if that is done with
// pre-positioned glyphs) // pre-positioned glyphs)
@ -164,7 +164,7 @@ impl IcedRenderer {
//self.current_scissor = default_scissor(renderer); //self.current_scissor = default_scissor(renderer);
self.draw_primitive(primitive, renderer); self.draw_primitive(primitive, Vec2::zero(), renderer);
// Enter the final command. // Enter the final command.
self.draw_commands.push(match self.current_state { self.draw_commands.push(match self.current_state {
@ -240,19 +240,29 @@ impl IcedRenderer {
let glyphs = &self.glyphs; let glyphs = &self.glyphs;
let mesh = &mut self.mesh; let mesh = &mut self.mesh;
let p_scale = self.p_scale;
let half_res = self.half_res;
glyphs glyphs
.iter() .iter()
.flat_map(|(mesh_index, glyph_count, linear_color)| { .flat_map(|(mesh_index, glyph_count, linear_color, offset)| {
let mesh_index = *mesh_index; let mesh_index = *mesh_index;
let linear_color = *linear_color; let linear_color = *linear_color;
(0..*glyph_count).map(move |i| (mesh_index + i * 6, linear_color)) // Could potentially pass this in as part of the extras
let offset = offset.map(|e| e as f32 * p_scale) / half_res;
(0..*glyph_count).map(move |i| (mesh_index + i * 6, linear_color, offset))
}) })
.zip(self.last_glyph_verts.iter()) .zip(self.last_glyph_verts.iter())
.for_each(|((mesh_index, linear_color), (uv, rect))| { .for_each(|((mesh_index, linear_color, offset), (uv, rect))| {
// TODO: add function to vek for this
let rect = Aabr {
min: rect.min + offset,
max: rect.max + offset,
};
mesh.replace_quad( mesh.replace_quad(
mesh_index, mesh_index,
create_ui_quad(*rect, *uv, linear_color, UiMode::Text), create_ui_quad(rect, *uv, linear_color, UiMode::Text),
) )
}); });
}, },
@ -344,12 +354,12 @@ impl IcedRenderer {
Aabr { min, max } Aabr { min, max }
} }
fn draw_primitive(&mut self, primitive: Primitive, renderer: &mut Renderer) { fn draw_primitive(&mut self, primitive: Primitive, offset: Vec2<u32>, renderer: &mut Renderer) {
match primitive { match primitive {
Primitive::Group { primitives } => { Primitive::Group { primitives } => {
primitives primitives
.into_iter() .into_iter()
.for_each(|p| self.draw_primitive(p, renderer)); .for_each(|p| self.draw_primitive(p, offset, renderer));
}, },
Primitive::Image { Primitive::Image {
handle, handle,
@ -363,7 +373,11 @@ impl IcedRenderer {
} }
let (graphic_id, rotation) = handle; let (graphic_id, rotation) = handle;
let gl_aabr = self.gl_aabr(bounds); let gl_aabr = self.gl_aabr(iced::Rectangle {
x: bounds.x + offset.x as f32,
y: bounds.y + offset.y as f32,
..bounds
});
let graphic_cache = self.cache.graphic_cache_mut(); let graphic_cache = self.cache.graphic_cache_mut();
@ -435,8 +449,14 @@ impl IcedRenderer {
self.switch_state(State::Plain); self.switch_state(State::Plain);
let gl_aabr = self.gl_aabr(iced::Rectangle {
x: bounds.x + offset.x as f32,
y: bounds.y + offset.y as f32,
..bounds
});
self.mesh.push_quad(create_ui_quad( self.mesh.push_quad(create_ui_quad(
self.gl_aabr(bounds), gl_aabr,
Aabr { Aabr {
min: Vec2::zero(), min: Vec2::zero(),
max: Vec2::zero(), max: Vec2::zero(),
@ -470,7 +490,7 @@ impl IcedRenderer {
// Since we already passed in `bounds` to position the glyphs some of this // Since we already passed in `bounds` to position the glyphs some of this
// seems redundant... // seems redundant...
// Note: we can't actually use this because dropping glyphs messeses up the // Note: we can't actually use this because dropping glyphs messeses up the
// counting and there is not a convenient method provided to drop out of bounds // counting and there is not a method provided to drop out of bounds
// glyphs while positioning them // glyphs while positioning them
glyph_brush::ab_glyph::Rect { glyph_brush::ab_glyph::Rect {
min: glyph_brush::ab_glyph::point( min: glyph_brush::ab_glyph::point(
@ -489,8 +509,12 @@ impl IcedRenderer {
min: Vec2::broadcast(0.0), min: Vec2::broadcast(0.0),
max: Vec2::broadcast(0.0), max: Vec2::broadcast(0.0),
}; };
self.glyphs self.glyphs.push((
.push((self.mesh.vertices().len(), glyph_count, linear_color)); self.mesh.vertices().len(),
glyph_count,
linear_color,
offset,
));
for _ in 0..glyph_count { for _ in 0..glyph_count {
// Push placeholder quad // Push placeholder quad
// Note: moving to some sort of layering / z based system would be an // Note: moving to some sort of layering / z based system would be an
@ -504,8 +528,13 @@ impl IcedRenderer {
)); ));
} }
}, },
Primitive::Clip { bounds, content } => { Primitive::Clip {
bounds,
offset: clip_offset,
content,
} => {
let new_scissor = { let new_scissor = {
// TODO: incorporate current offset for nested Clips
let intersection = Aabr { let intersection = Aabr {
min: Vec2 { min: Vec2 {
x: (bounds.x * self.p_scale) as u16, x: (bounds.x * self.p_scale) as u16,
@ -545,7 +574,7 @@ impl IcedRenderer {
// TODO: cull primitives outside the current scissor // TODO: cull primitives outside the current scissor
// Renderer child // Renderer child
self.draw_primitive(*content, renderer); self.draw_primitive(*content, offset + clip_offset, renderer);
// Reset scissor // Reset scissor
self.draw_commands.push(match self.current_state { self.draw_commands.push(match self.current_state {

View File

@ -25,6 +25,7 @@ pub enum Primitive {
}, },
Clip { Clip {
bounds: iced::Rectangle, bounds: iced::Rectangle,
offset: vek::Vec2<u32>,
content: Box<Primitive>, content: Box<Primitive>,
}, },
Nothing, Nothing,

View File

@ -1,2 +1,3 @@
pub mod button; pub mod button;
pub mod container; pub mod container;
pub mod scrollable;

View File

@ -0,0 +1,33 @@
use super::super::super::widget::image;
use vek::Rgba;
#[derive(Clone, Copy)]
pub struct Style {
pub track: Option<Track>,
pub scroller: Scroller,
}
impl Default for Style {
fn default() -> Self {
Self {
track: None,
scroller: Scroller::Color(Rgba::new(128, 128, 128, 255)),
}
}
}
#[derive(Clone, Copy)]
pub enum Track {
Color(Rgba<u8>),
Image(image::Handle, Rgba<u8>),
}
#[derive(Clone, Copy)]
pub enum Scroller {
Color(Rgba<u8>),
Image {
ends: image::Handle,
mid: image::Handle,
color: Rgba<u8>,
},
}

View File

@ -6,6 +6,7 @@ mod compound_graphic;
mod container; mod container;
mod image; mod image;
mod row; mod row;
mod scrollable;
mod space; mod space;
mod text; mod text;
mod text_input; mod text_input;

View File

@ -0,0 +1,178 @@
use super::super::{super::Rotation, style, IcedRenderer, Primitive};
use common::util::srgba_to_linear;
use iced::{mouse, scrollable, Rectangle};
use style::scrollable::{Scroller, Track};
const SCROLLBAR_WIDTH: u16 = 10;
const SCROLLBAR_MIN_HEIGHT: u16 = 6;
const SCROLLBAR_MARGIN: u16 = 2;
impl scrollable::Renderer for IcedRenderer {
type Style = style::scrollable::Style;
// Interesting that this is here
// I guess we can take advantage of this to keep a constant size despite
// scaling?
fn scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
offset: u32,
) -> Option<scrollable::Scrollbar> {
// TODO: might actually want to divide by p_scale here (same in text&ext_input)
// (or just not use it) (or at least only account for dpi but not any
// additional scaling)
let width = (SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN) as f32 * self.p_scale;
if content_bounds.height > bounds.height {
let scrollbar_bounds = Rectangle {
x: bounds.x + bounds.width - width,
width,
..bounds
};
let visible_fraction = bounds.height / content_bounds.height;
let scrollbar_height = (bounds.height * visible_fraction)
.max((2 * SCROLLBAR_MIN_HEIGHT) as f32 * self.p_scale);
let y_offset = offset as f32 * visible_fraction;
let scroller_bounds = Rectangle {
x: scrollbar_bounds.x + SCROLLBAR_MARGIN as f32 * self.p_scale,
// TODO: check this behavior
y: scrollbar_bounds.y + y_offset,
width: scrollbar_bounds.width - (2 * SCROLLBAR_MARGIN) as f32 * self.p_scale,
height: scrollbar_height,
};
Some(scrollable::Scrollbar {
bounds: scrollbar_bounds,
scroller: scrollable::Scroller {
bounds: scroller_bounds,
},
})
} else {
None
}
}
fn draw(
&mut self,
state: &scrollable::State,
bounds: Rectangle,
_content_bounds: Rectangle,
is_mouse_over: bool,
is_mouse_over_scrollbar: bool,
scrollbar: Option<scrollable::Scrollbar>,
offset: u32,
style_sheet: &Self::Style,
(content, mouse_interaction): Self::Output,
) -> Self::Output {
(
if let Some(scrollbar) = scrollbar {
let mut primitives = Vec::with_capacity(5);
// Scrolled content
primitives.push(Primitive::Clip {
bounds,
offset: (0, offset).into(),
content: Box::new(content),
});
let style = style_sheet;
//let style = if state.is_scroller_grabbed() {
// style_sheet.dragging()
//} else if is_mouse_over_scrollbar {
// style_sheet.hovered()
//} else {
// style_sheet.active();
//};
let is_scrollbar_visible = style.track.is_some();
if is_mouse_over || state.is_scroller_grabbed() || is_scrollbar_visible {
let bounds = scrollbar.scroller.bounds;
match style.scroller {
Scroller::Color(color) => primitives.push(Primitive::Rectangle {
bounds,
linear_color: srgba_to_linear(color.map(|e| e as f32 / 255.0)),
}),
Scroller::Image { ends, mid, color } => {
// Calculate sizes of ends pieces based on image aspect ratio
let (img_w, img_h) = self.image_dims(ends);
let end_height = bounds.width * img_h as f32 / img_w as f32;
// Calcutate size of middle piece based on available space
// Note: Might want to scale into real pixels for parts of this
let (end_height, middle_height) =
if end_height * 2.0 + 1.0 <= bounds.height {
(end_height, bounds.height - end_height * 2.0)
} else {
// Take 1 logical pixel for the middle height
let remaining_height = bounds.height - 1.0;
(remaining_height / 2.0, 1.0)
};
// Top
primitives.push(Primitive::Image {
handle: (ends, Rotation::None),
bounds: Rectangle {
height: end_height,
..bounds
},
color,
});
// Middle
primitives.push(Primitive::Image {
handle: (mid, Rotation::None),
bounds: Rectangle {
y: bounds.y + end_height,
height: middle_height,
..bounds
},
color,
});
// Bottom
primitives.push(Primitive::Image {
handle: (ends, Rotation::Cw180),
bounds: Rectangle {
y: bounds.y + end_height + middle_height,
height: end_height,
..bounds
},
color,
});
},
}
}
if let Some(track) = style.track {
let bounds = Rectangle {
x: scrollbar.bounds.x + SCROLLBAR_MARGIN as f32 * self.p_scale,
width: scrollbar.bounds.width
- (2 * SCROLLBAR_MARGIN) as f32 * self.p_scale,
..scrollbar.bounds
};
primitives.push(match track {
Track::Color(color) => Primitive::Rectangle {
bounds,
linear_color: srgba_to_linear(color.map(|e| e as f32 / 255.0)),
},
Track::Image(handle, color) => Primitive::Image {
handle: (handle, Rotation::None),
bounds,
color,
},
});
}
Primitive::Group { primitives }
} else {
content
},
if is_mouse_over_scrollbar || state.is_scroller_grabbed() {
mouse::Interaction::Idle
} else {
mouse_interaction
},
)
}
}

View File

@ -169,7 +169,7 @@ impl text_input::Renderer for IcedRenderer {
( (
Primitive::Rectangle { Primitive::Rectangle {
bounds: Rectangle { bounds: Rectangle {
x: text_bounds.x + position - offset - CURSOR_WIDTH / p_scale / 2.0, x: text_bounds.x + position - CURSOR_WIDTH / p_scale / 2.0,
y: text_bounds.y, y: text_bounds.y,
width: CURSOR_WIDTH / p_scale, width: CURSOR_WIDTH / p_scale,
height: text_bounds.height, height: text_bounds.height,
@ -196,7 +196,7 @@ impl text_input::Renderer for IcedRenderer {
( (
Primitive::Rectangle { Primitive::Rectangle {
bounds: Rectangle { bounds: Rectangle {
x: text_bounds.x + left_position - left_offset, x: text_bounds.x + left_position,
y: text_bounds.y, y: text_bounds.y,
width, width,
height: text_bounds.height, height: text_bounds.height,
@ -216,10 +216,9 @@ impl text_input::Renderer for IcedRenderer {
let display_text = text.unwrap_or(if state.is_focused() { "" } else { placeholder }); let display_text = text.unwrap_or(if state.is_focused() { "" } else { placeholder });
let section = glyph_brush::Section { let section = glyph_brush::Section {
screen_position: ( // Note: clip offset is an integer so we don't have to worry about not accounting for
text_bounds.x * p_scale - scroll_offset, // that here where the alignment of the glyphs with pixels affects rasterization
text_bounds.center_y() * p_scale, screen_position: (text_bounds.x * p_scale, text_bounds.center_y() * p_scale),
),
bounds: ( bounds: (
10000.0, /* text_bounds.width * p_scale */ 10000.0, /* text_bounds.width * p_scale */
text_bounds.height * p_scale, text_bounds.height * p_scale,
@ -279,9 +278,8 @@ impl text_input::Renderer for IcedRenderer {
let primitive = if text_width > text_bounds.width { let primitive = if text_width > text_bounds.width {
Primitive::Clip { Primitive::Clip {
bounds: text_bounds, bounds: text_bounds,
offset: (scroll_offset as u32, 0).into(),
content: Box::new(primitive), content: Box::new(primitive),
/* Note: iced_wgpu uses offset here but we can't do that since we pass the text
* to the glyph_brush here */
} }
} else { } else {
primitive primitive