diff --git a/assets/voxygen/element/frames/tt_test_corner_tr.vox b/assets/voxygen/element/frames/tt_test_corner_tr.vox new file mode 100644 index 0000000000..7e01af32ad --- /dev/null +++ b/assets/voxygen/element/frames/tt_test_corner_tr.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce9a50b44f6cc2b6d0da034d1c4bb953027d12ef997b63514a97391226a475f7 +size 45806 diff --git a/assets/voxygen/element/frames/tt_test_edge.vox b/assets/voxygen/element/frames/tt_test_edge.vox new file mode 100644 index 0000000000..53c27cb560 --- /dev/null +++ b/assets/voxygen/element/frames/tt_test_edge.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f788813e95126cc6eb9a8afdd7eb906690e63270e192e7bc2c4335b27e21dbe2 +size 45658 diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index 0b98ca0f04..76121b4fea 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -56,7 +56,7 @@ pub fn rgb_to_hsv(rgb: Rgb) -> Vec3 { let h = if max == min { 0.0 } else { - let mut h = (60.0 * (add + diff / (max - min))); + let mut h = 60.0 * (add + diff / (max - min)); if h < 0.0 { h += 360.0; } diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index b1602adc72..aeeaf36bc5 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -3,7 +3,7 @@ use crate::{ ui::{ self, img_ids::{BlankGraphic, ImageGraphic, VoxelGraphic}, - Tooltip, Tooltipable, Ui, + ImageFrame, Tooltip, Tooltipable, Ui, }, GlobalState, }; @@ -78,7 +78,16 @@ image_ids! { nothing: (), } +} +rotation_image_ids! { + pub struct ImgsRot { + + + // Tooltip Test + tt_side: "voxygen/element/frames/tt_test_edge", + tt_corner: "voxygen/element/frames/tt_test_corner_tr", + } } font_ids! { @@ -104,6 +113,7 @@ pub struct MainMenuUi { ui: Ui, ids: Ids, imgs: Imgs, + rot_imgs: ImgsRot, fonts: Fonts, username: String, password: String, @@ -126,6 +136,7 @@ impl MainMenuUi { let ids = Ids::new(ui.id_generator()); // Load images let imgs = Imgs::load(&mut ui).expect("Failed to load images"); + let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load images!"); // Load fonts let fonts = Fonts::load(&mut ui).expect("Failed to load fonts"); @@ -133,6 +144,7 @@ impl MainMenuUi { ui, ids, imgs, + rot_imgs, fonts, username: networking.username.clone(), password: "".to_owned(), @@ -150,6 +162,25 @@ impl MainMenuUi { let version = env!("CARGO_PKG_VERSION"); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2); + + // Tooltip + let tooltip = Tooltip::new({ + // Edge images [t, b, r, l] + // Corner images [tr, tl, br, bl] + let edge = &self.rot_imgs.tt_side; + let corner = &self.rot_imgs.tt_corner; + ImageFrame::new( + [edge.cw180, edge.none, edge.cw270, edge.cw90], + [corner.none, corner.cw270, corner.cw90, corner.cw180], + Color::Rgba(0.08, 0.07, 0.04, 1.0), + 5.0, + ) + }) + .title_font_size(15) + .desc_font_size(10) + .title_text_color(TEXT_COLOR) + .desc_text_color(TEXT_COLOR_2); + // Background image, Veloren logo, Alpha-Version Label Image::new(self.imgs.bg) .middle_of(ui_widgets.window) @@ -158,6 +189,7 @@ impl MainMenuUi { .w_h(123.0 * 3.0, 35.0 * 3.0) .top_right_with_margins(30.0, 30.0) .set(self.ids.v_logo, ui_widgets); + Text::new(version) .top_right_with_margins_on(ui_widgets.window, 5.0, 5.0) .font_size(14) @@ -467,12 +499,11 @@ impl MainMenuUi { .label_y(Relative::Scalar(5.0)) .with_tooltip( tooltip_manager, - Tooltip::new("Login", "Click to login with the entered details") - .title_font_size(15) - .desc_font_size(10) - .title_text_color(TEXT_COLOR) - .desc_text_color(TEXT_COLOR_2), + "Login", + "Click to login with the entered details", + &tooltip, ) + .tooltip_image(self.imgs.v_logo) .set(self.ids.login_button, ui_widgets) .was_clicked() { diff --git a/voxygen/src/render/pipelines/ui.rs b/voxygen/src/render/pipelines/ui.rs index 246d32f5bd..8787def27a 100644 --- a/voxygen/src/render/pipelines/ui.rs +++ b/voxygen/src/render/pipelines/ui.rs @@ -97,12 +97,27 @@ pub fn create_quad( let (l, b, r, t) = aabr_to_lbrt(rect); let (uv_l, uv_b, uv_r, uv_t) = aabr_to_lbrt(uv_rect); - Quad::new( - v([r, t], [uv_r, uv_t]), - v([l, t], [uv_l, uv_t]), - v([l, b], [uv_l, uv_b]), - v([r, b], [uv_r, uv_b]), - ) + + match (uv_b > uv_t, uv_l > uv_r) { + (true, true) => Quad::new( + v([r, t], [uv_l, uv_b]), + v([l, t], [uv_l, uv_t]), + v([l, b], [uv_r, uv_t]), + v([r, b], [uv_r, uv_b]), + ), + (false, false) => Quad::new( + v([r, t], [uv_l, uv_b]), + v([l, t], [uv_l, uv_t]), + v([l, b], [uv_r, uv_t]), + v([r, b], [uv_r, uv_b]), + ), + _ => Quad::new( + v([r, t], [uv_r, uv_t]), + v([l, t], [uv_l, uv_t]), + v([l, b], [uv_l, uv_b]), + v([r, b], [uv_r, uv_b]), + ), + } } pub fn create_tri( diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 347b4a6cdb..021ab169fa 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -186,7 +186,7 @@ impl Renderer { ) } - /// Queue the clearing of the color and depth targets ready for a new frame to be rendered. + /// Queue the clearing of the depth target ready for a new frame to be rendered. pub fn clear(&mut self) { self.encoder.clear_depth(&self.tgt_depth_view, 1.0); self.encoder.clear_depth(&self.win_depth_view, 1.0); diff --git a/voxygen/src/ui/graphic/graphic.rs b/voxygen/src/ui/graphic/graphic.rs deleted file mode 100644 index c830be20b3..0000000000 --- a/voxygen/src/ui/graphic/graphic.rs +++ /dev/null @@ -1,246 +0,0 @@ -use dot_vox::DotVoxData; -use guillotiere::{size2, AllocId, Allocation, AtlasAllocator}; -use hashbrown::HashMap; -use image::{DynamicImage, RgbaImage}; -use log::{error, warn}; -use std::sync::Arc; -use vek::*; - -#[derive(Clone)] -pub enum Graphic { - Image(Arc), - Voxel(Arc, Option), - Blank, -} - -#[derive(PartialEq, Eq, Hash, Copy, Clone)] -pub struct Id(u32); - -type Parameters = (Id, Vec2, Aabr); - -pub struct CachedDetails { - // Id used by AtlasAllocator - alloc_id: AllocId, - // Last frame this was used on - frame: u32, - // Where in the cache texture this is - aabr: Aabr, -} - -pub struct GraphicCache { - graphic_map: HashMap, - next_id: u32, - - atlas: AtlasAllocator, - cache_map: HashMap, - // The current frame - current_frame: u32, - unused_entries_this_frame: Option>>, - - soft_cache: HashMap, - transfer_ready: Vec<(Parameters, Aabr)>, -} -impl GraphicCache { - pub fn new(size: Vec2) -> Self { - Self { - graphic_map: HashMap::default(), - next_id: 0, - atlas: AtlasAllocator::new(size2(i32::from(size.x), i32::from(size.y))), - cache_map: HashMap::default(), - current_frame: 0, - unused_entries_this_frame: None, - soft_cache: HashMap::default(), - transfer_ready: Vec::new(), - } - } - pub fn add_graphic(&mut self, graphic: Graphic) -> Id { - let id = self.next_id; - self.next_id = id.wrapping_add(1); - - let id = Id(id); - self.graphic_map.insert(id, graphic); - - id - } - pub fn get_graphic(&self, id: Id) -> Option<&Graphic> { - self.graphic_map.get(&id) - } - pub fn clear_cache(&mut self, new_size: Vec2) { - self.soft_cache.clear(); - self.transfer_ready.clear(); - self.cache_map.clear(); - self.atlas = AtlasAllocator::new(size2(i32::from(new_size.x), i32::from(new_size.y))); - } - - pub fn queue_res( - &mut self, - graphic_id: Id, - dims: Vec2, - source: Aabr, - ) -> Option> { - let key = (graphic_id, dims, source.map(|e| e.to_bits())); // TODO: Replace this with rounded representation of source - - if let Some(details) = self.cache_map.get_mut(&key) { - // Update frame - details.frame = self.current_frame; - - Some(details.aabr) - } else { - // Create image if it doesn't already exist - if !self.soft_cache.contains_key(&key) { - self.soft_cache.insert( - key, - match self.graphic_map.get(&graphic_id) { - Some(Graphic::Blank) => return None, - // Render image at requested resolution - // TODO: Use source aabr. - Some(Graphic::Image(ref image)) => image - .resize_exact( - u32::from(dims.x), - u32::from(dims.y), - image::FilterType::Nearest, - ) - .to_rgba(), - Some(Graphic::Voxel(ref vox, min_samples)) => { - super::renderer::draw_vox(&vox.as_ref().into(), dims, *min_samples) - } - None => { - warn!("A graphic was requested via an id which is not in use"); - return None; - } - }, - ); - } - - let aabr_from_alloc_rect = |rect: guillotiere::Rectangle| { - let (min, max) = (rect.min, rect.max); - Aabr { - min: Vec2::new(min.x as u16, min.y as u16), - max: Vec2::new(max.x as u16, max.y as u16), - } - }; - - // Allocate rectangle. - let (alloc_id, aabr) = match self - .atlas - .allocate(size2(i32::from(dims.x), i32::from(dims.y))) - { - Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), - // Out of room. - // 1) Remove unused allocations - // TODO: Make more room. - // 2) Rearrange rectangles (see comments below) - // 3) Expand cache size - None => { - // 1) Remove unused allocations - if self.unused_entries_this_frame.is_none() { - self.unused_entries_this_frame = { - let mut unused = self - .cache_map - .iter() - .filter_map(|(key, details)| { - if details.frame < self.current_frame - 1 { - Some(Some((details.frame, *key))) - } else { - None - } - }) - .collect::>(); - unused - .sort_unstable_by(|a, b| a.map(|(f, _)| f).cmp(&b.map(|(f, _)| f))); - Some(unused) - }; - } - - let mut allocation = None; - // Fight the checker! - let current_frame = self.current_frame; - // Will always be Some - if let Some(ref mut unused_entries) = self.unused_entries_this_frame { - // Deallocate from oldest to newest - for key in unused_entries - .iter_mut() - .filter_map(|e| e.take().map(|(_, key)| key)) - { - // Check if still in cache map and it has not been used since the vec was built - if self - .cache_map - .get(&key) - .filter(|d| d.frame != current_frame) - .is_some() - { - if let Some(alloc_id) = - self.cache_map.remove(&key).map(|d| d.alloc_id) - { - // Deallocate - self.atlas.deallocate(alloc_id); - // Try to allocate - if let Some(alloc) = self - .atlas - .allocate(size2(i32::from(dims.x), i32::from(dims.y))) - { - allocation = Some(alloc); - break; - } - } - } - } - // 2) Rearrange rectangles - // This needs to be done infrequently and be based on whether rectangles have been removed - // Maybe find a way to calculate whether there is a significant amount of fragmentation - // Or consider dropping the use of an atlas and moving to a hashmap of individual textures :/ - // if allocation.is_none() { - // - // } - } - - match allocation { - Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), - None => { - warn!("Can't find space for an image in the graphic cache"); - return None; - } - } - } - }; - self.transfer_ready.push((key, aabr)); - - // Insert area into map for retrieval. - self.cache_map.insert( - key, - CachedDetails { - alloc_id, - frame: self.current_frame, - aabr, - }, - ); - - Some(aabr) - } - } - - // Anything not queued since the last call to this will be removed if there is not enough space in the cache - pub fn cache_queued(&mut self, mut cacher: F) - where - F: FnMut(Aabr, &[[u8; 4]]), - { - // Cached queued - // TODO: combine nearby transfers - for (key, target_aarb) in self.transfer_ready.drain(..) { - if let Some(image) = self.soft_cache.get(&key) { - cacher( - target_aarb, - &image.pixels().map(|p| p.0).collect::>(), - ); - } else { - error!("Image queued for transfer to gpu cache but it doesn't exist (this should never occur)"); - } - } - - // Increment frame - self.current_frame += 1; - - // Reset unused entries - self.unused_entries_this_frame = None; - } -} diff --git a/voxygen/src/ui/graphic/mod.rs b/voxygen/src/ui/graphic/mod.rs index a7b96f903f..c4707fdefc 100644 --- a/voxygen/src/ui/graphic/mod.rs +++ b/voxygen/src/ui/graphic/mod.rs @@ -1,4 +1,274 @@ -mod graphic; mod renderer; -pub use graphic::{Graphic, GraphicCache, Id}; +use dot_vox::DotVoxData; +use guillotiere::{size2, AllocId, Allocation, AtlasAllocator}; +use hashbrown::HashMap; +use image::{DynamicImage, RgbaImage}; +use log::{error, warn}; +use std::sync::Arc; +use vek::*; + +#[derive(Clone)] +pub enum Graphic { + Image(Arc), + Voxel(Arc, Option>, Option), + Blank, +} + +#[derive(Clone, Copy)] +pub enum Rotation { + None, + Cw90, + Cw180, + Cw270, +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone)] +pub struct Id(u32); + +type Parameters = (Id, Vec2, Aabr); + +struct CachedDetails { + // Id used by AtlasAllocator + alloc_id: AllocId, + // Last frame this was used on + frame: u32, + // Where in the cache texture this is + aabr: Aabr, +} + +pub struct GraphicCache { + graphic_map: HashMap, + next_id: u32, + + atlas: AtlasAllocator, + cache_map: HashMap, + // The current frame + current_frame: u32, + unused_entries_this_frame: Option>>, + + soft_cache: HashMap, + transfer_ready: Vec<(Parameters, Aabr)>, +} +impl GraphicCache { + pub fn new(size: Vec2) -> Self { + Self { + graphic_map: HashMap::default(), + next_id: 0, + atlas: AtlasAllocator::new(size2(i32::from(size.x), i32::from(size.y))), + cache_map: HashMap::default(), + current_frame: 0, + unused_entries_this_frame: None, + soft_cache: HashMap::default(), + transfer_ready: Vec::new(), + } + } + pub fn add_graphic(&mut self, graphic: Graphic) -> Id { + let id = self.next_id; + self.next_id = id.wrapping_add(1); + + let id = Id(id); + self.graphic_map.insert(id, graphic); + + id + } + pub fn get_graphic(&self, id: Id) -> Option<&Graphic> { + self.graphic_map.get(&id) + } + pub fn clear_cache(&mut self, new_size: Vec2) { + self.soft_cache.clear(); + self.transfer_ready.clear(); + self.cache_map.clear(); + self.atlas = AtlasAllocator::new(size2(i32::from(new_size.x), i32::from(new_size.y))); + } + + pub fn queue_res( + &mut self, + graphic_id: Id, + dims: Vec2, + source: Aabr, + rotation: Rotation, + ) -> Option> { + let dims = match rotation { + Rotation::Cw90 | Rotation::Cw270 => Vec2::new(dims.y, dims.x), + Rotation::None | Rotation::Cw180 => dims, + }; + let key = (graphic_id, dims, source.map(|e| e.to_bits())); // TODO: Replace this with rounded representation of source + + let rotated_aabr = |Aabr { min, max }| match rotation { + Rotation::None => Aabr { min, max }, + Rotation::Cw90 => Aabr { + min: Vec2::new(min.x, max.y), + max: Vec2::new(max.x, min.y), + }, + Rotation::Cw180 => Aabr { min: max, max: min }, + Rotation::Cw270 => Aabr { + min: Vec2::new(max.x, min.y), + max: Vec2::new(min.x, max.y), + }, + }; + + if let Some(details) = self.cache_map.get_mut(&key) { + // Update frame + details.frame = self.current_frame; + + Some(rotated_aabr(details.aabr)) + } else { + // Create image if it doesn't already exist + if !self.soft_cache.contains_key(&key) { + self.soft_cache.insert( + key, + match self.graphic_map.get(&graphic_id) { + Some(Graphic::Blank) => return None, + // Render image at requested resolution + // TODO: Use source aabr. + Some(Graphic::Image(ref image)) => image + .resize_exact( + u32::from(dims.x), + u32::from(dims.y), + image::FilterType::Nearest, + ) + .to_rgba(), + Some(Graphic::Voxel(ref vox, ori, min_samples)) => { + renderer::draw_vox(&vox.as_ref().into(), dims, *ori, *min_samples) + } + None => { + warn!("A graphic was requested via an id which is not in use"); + return None; + } + }, + ); + } + + let aabr_from_alloc_rect = |rect: guillotiere::Rectangle| { + let (min, max) = (rect.min, rect.max); + Aabr { + min: Vec2::new(min.x as u16, min.y as u16), + max: Vec2::new(max.x as u16, max.y as u16), + } + }; + + // Allocate rectangle. + let (alloc_id, aabr) = match self + .atlas + .allocate(size2(i32::from(dims.x), i32::from(dims.y))) + { + Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), + // Out of room. + // 1) Remove unused allocations + // TODO: Make more room. + // 2) Rearrange rectangles (see comments below) + // 3) Expand cache size + None => { + // 1) Remove unused allocations + if self.unused_entries_this_frame.is_none() { + self.unused_entries_this_frame = { + let mut unused = self + .cache_map + .iter() + .filter_map(|(key, details)| { + if details.frame < self.current_frame - 1 { + Some(Some((details.frame, *key))) + } else { + None + } + }) + .collect::>(); + unused + .sort_unstable_by(|a, b| a.map(|(f, _)| f).cmp(&b.map(|(f, _)| f))); + Some(unused) + }; + } + + let mut allocation = None; + // Fight the checker! + let current_frame = self.current_frame; + // Will always be Some + if let Some(ref mut unused_entries) = self.unused_entries_this_frame { + // Deallocate from oldest to newest + for key in unused_entries + .iter_mut() + .filter_map(|e| e.take().map(|(_, key)| key)) + { + // Check if still in cache map and it has not been used since the vec was built + if self + .cache_map + .get(&key) + .filter(|d| d.frame != current_frame) + .is_some() + { + if let Some(alloc_id) = + self.cache_map.remove(&key).map(|d| d.alloc_id) + { + // Deallocate + self.atlas.deallocate(alloc_id); + // Try to allocate + if let Some(alloc) = self + .atlas + .allocate(size2(i32::from(dims.x), i32::from(dims.y))) + { + allocation = Some(alloc); + break; + } + } + } + } + // 2) Rearrange rectangles + // This needs to be done infrequently and be based on whether rectangles have been removed + // Maybe find a way to calculate whether there is a significant amount of fragmentation + // Or consider dropping the use of an atlas and moving to a hashmap of individual textures :/ + // if allocation.is_none() { + // + // } + } + + match allocation { + Some(Allocation { id, rectangle }) => (id, aabr_from_alloc_rect(rectangle)), + None => { + warn!("Can't find space for an image in the graphic cache"); + return None; + } + } + } + }; + self.transfer_ready.push((key, aabr)); + + // Insert area into map for retrieval. + self.cache_map.insert( + key, + CachedDetails { + alloc_id, + frame: self.current_frame, + aabr, + }, + ); + + Some(rotated_aabr(aabr)) + } + } + + // Anything not queued since the last call to this will be removed if there is not enough space in the cache + pub fn cache_queued(&mut self, mut cacher: F) + where + F: FnMut(Aabr, &[[u8; 4]]), + { + // Cached queued + // TODO: combine nearby transfers + for (key, target_aarb) in self.transfer_ready.drain(..) { + if let Some(image) = self.soft_cache.get(&key) { + cacher( + target_aarb, + &image.pixels().map(|p| p.0).collect::>(), + ); + } else { + error!("Image queued for transfer to gpu cache but it doesn't exist (this should never occur)"); + } + } + + // Increment frame + self.current_frame += 1; + + // Reset unused entries + self.unused_entries_this_frame = None; + } +} diff --git a/voxygen/src/ui/graphic/renderer.rs b/voxygen/src/ui/graphic/renderer.rs index 0542d8b268..ecd6a8ebb5 100644 --- a/voxygen/src/ui/graphic/renderer.rs +++ b/voxygen/src/ui/graphic/renderer.rs @@ -58,7 +58,12 @@ impl<'a> Pipeline for Voxel { } } -pub fn draw_vox(segment: &Segment, output_size: Vec2, min_samples: Option) -> RgbaImage { +pub fn draw_vox( + segment: &Segment, + output_size: Vec2, + ori: Option>, + min_samples: Option, +) -> RgbaImage { let scale = min_samples.map_or(1.0, |s| s as f32).sqrt().ceil() as usize; let dims = output_size.map(|e| e as usize * scale).into_array(); let mut color = Buffer2d::new(dims, [0; 4]); @@ -73,7 +78,8 @@ pub fn draw_vox(segment: &Segment, output_size: Vec2, min_samples: Option, _>( diff --git a/voxygen/src/ui/img_ids.rs b/voxygen/src/ui/img_ids.rs index f2c513198f..4dbba78796 100644 --- a/voxygen/src/ui/img_ids.rs +++ b/voxygen/src/ui/img_ids.rs @@ -31,7 +31,7 @@ pub enum VoxelMs9Graphic {} impl<'a> GraphicCreator<'a> for VoxelGraphic { type Specifier = &'a str; fn new_graphic(specifier: Self::Specifier) -> Result { - Ok(Graphic::Voxel(load::(specifier)?, None)) + Ok(Graphic::Voxel(load::(specifier)?, None, None)) } } impl<'a> GraphicCreator<'a> for VoxelMsGraphic { @@ -39,6 +39,7 @@ impl<'a> GraphicCreator<'a> for VoxelMsGraphic { fn new_graphic(specifier: Self::Specifier) -> Result { Ok(Graphic::Voxel( load::(specifier.0)?, + None, Some(specifier.1), )) } @@ -46,16 +47,31 @@ impl<'a> GraphicCreator<'a> for VoxelMsGraphic { impl<'a> GraphicCreator<'a> for VoxelMs4Graphic { type Specifier = &'a str; fn new_graphic(specifier: Self::Specifier) -> Result { - Ok(Graphic::Voxel(load::(specifier)?, Some(4))) + Ok(Graphic::Voxel( + load::(specifier)?, + None, + Some(4), + )) } } impl<'a> GraphicCreator<'a> for VoxelMs9Graphic { type Specifier = &'a str; fn new_graphic(specifier: Self::Specifier) -> Result { - Ok(Graphic::Voxel(load::(specifier)?, Some(9))) + Ok(Graphic::Voxel( + load::(specifier)?, + None, + Some(9), + )) } } +pub struct Rotations { + pub none: conrod_core::image::Id, + pub cw90: conrod_core::image::Id, + pub cw180: conrod_core::image::Id, + pub cw270: conrod_core::image::Id, +} + /// This macro will automatically load all specified assets, get the corresponding ImgIds and /// create a struct with all of them. /// @@ -80,7 +96,7 @@ macro_rules! image_ids { ($($v:vis struct $Ids:ident { $( <$T:ty> $( $name:ident: $specifier:expr ),* $(,)? )* })*) => { $( $v struct $Ids { - $($( $v $name: conrod_core::image::Id, )*)* + $($( $v $name: conrod_core::image::Id, )*)* } impl $Ids { @@ -94,3 +110,24 @@ macro_rules! image_ids { )* }; } + +// TODO: combine with the img_ids macro above using a marker for specific fields that should be `Rotations` instead of `widget::Id` +#[macro_export] +macro_rules! rotation_image_ids { + ($($v:vis struct $Ids:ident { $( <$T:ty> $( $name:ident: $specifier:expr ),* $(,)? )* })*) => { + $( + $v struct $Ids { + $($( $v $name: crate::ui::img_ids::Rotations, )*)* + } + + impl $Ids { + pub fn load(ui: &mut crate::ui::Ui) -> Result { + use crate::ui::img_ids::GraphicCreator; + Ok(Self { + $($( $name: ui.add_graphic_with_rotations(<$T as GraphicCreator>::new_graphic($specifier)?), )*)* + }) + } + } + )* + }; +} diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index d61c2c5006..4b7981f020 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -12,6 +12,7 @@ pub use event::Event; pub use graphic::Graphic; pub use scale::{Scale, ScaleMode}; pub use widgets::{ + image_frame::ImageFrame, image_slider::ImageSlider, ingame::{Ingame, IngameAnchor, Ingameable}, toggle_button::ToggleButton, @@ -38,7 +39,7 @@ use conrod_core::{ widget::{self, id::Generator}, Rect, UiBuilder, UiCell, }; -use graphic::Id as GraphicId; +use graphic::Rotation; use log::warn; use std::{ fs::File, @@ -92,7 +93,7 @@ impl assets::Asset for Font { pub struct Ui { ui: conrod_core::Ui, - image_map: Map, + image_map: Map<(graphic::Id, Rotation)>, cache: Cache, // Draw commands for the next render draw_commands: Vec, @@ -132,7 +133,7 @@ impl Ui { ui, image_map: Map::new(), cache: Cache::new(renderer)?, - draw_commands: vec![], + draw_commands: Vec::new(), model: renderer.create_dynamic_model(100)?, interface_locals: renderer.create_consts(&[UiLocals::default()])?, default_globals: renderer.create_consts(&[Globals::default()])?, @@ -160,7 +161,18 @@ impl Ui { } pub fn add_graphic(&mut self, graphic: Graphic) -> image::Id { - self.image_map.insert(self.cache.add_graphic(graphic)) + self.image_map + .insert((self.cache.add_graphic(graphic), Rotation::None)) + } + + pub fn add_graphic_with_rotations(&mut self, graphic: Graphic) -> img_ids::Rotations { + let graphic_id = self.cache.add_graphic(graphic); + img_ids::Rotations { + none: self.image_map.insert((graphic_id, Rotation::None)), + cw90: self.image_map.insert((graphic_id, Rotation::Cw90)), + cw180: self.image_map.insert((graphic_id, Rotation::Cw180)), + cw270: self.image_map.insert((graphic_id, Rotation::Cw270)), + } } pub fn new_font(&mut self, font: Arc) -> font::Id { @@ -257,6 +269,15 @@ impl Ui { self.draw_commands.clear(); let mut mesh = Mesh::new(); + let (half_res, x_align, y_align) = { + let res = renderer.get_resolution(); + ( + res.map(|e| e as f32 / 2.0), + (res.x & 1) as f32 * 0.5, + (res.y & 1) as f32 * 0.5, + ) + }; + // TODO: this could be removed entirely if the draw call just used both textures, // however this allows for flexibility if we want to interweave other draw calls later. enum State { @@ -372,10 +393,15 @@ impl Ui { let vy = |y: f64| (y / ui_win_h * 2.0) as f32; let gl_aabr = |rect: Rect| { let (l, r, b, t) = rect.l_r_b_t(); - Aabr { - min: Vec2::new(vx(l), vy(b)), - max: Vec2::new(vx(r), vy(t)), - } + let min = Vec2::new( + ((vx(l) * half_res.x + x_align).round() - x_align) / half_res.x, + ((vy(b) * half_res.y + y_align).round() - y_align) / half_res.y, + ); + let max = Vec2::new( + ((vx(r) * half_res.x + x_align).round() - x_align) / half_res.x, + ((vy(t) * half_res.y + y_align).round() - y_align) / half_res.y, + ); + Aabr { min, max } }; match kind { @@ -384,7 +410,7 @@ impl Ui { color, source_rect: _, // TODO: <-- use this } => { - let graphic_id = self + let (graphic_id, rotation) = self .image_map .get(&image_id) .expect("Image does not exist in image map"); @@ -406,9 +432,10 @@ impl Ui { let color = srgba_to_linear(color.unwrap_or(conrod_core::color::WHITE).to_fsa().into()); + let gl_aabr = gl_aabr(rect); let resolution = Vec2::new( - (rect.w() * p_scale_factor).round() as u16, - (rect.h() * p_scale_factor).round() as u16, + (gl_aabr.size().w * half_res.x).round() as u16, + (gl_aabr.size().h * half_res.y).round() as u16, ); // Transform the source rectangle into uv coordinate. // TODO: Make sure this is right. @@ -434,22 +461,26 @@ impl Ui { cache_tex.get_dimensions().map(|e| e as f32).into_tuple(); // Cache graphic at particular resolution. - let uv_aabr = - match graphic_cache.queue_res(*graphic_id, resolution, source_aabr) { - Some(aabr) => Aabr { - min: Vec2::new( - aabr.min.x as f32 / cache_w, - aabr.max.y as f32 / cache_h, - ), - max: Vec2::new( - aabr.max.x as f32 / cache_w, - aabr.min.y as f32 / cache_h, - ), - }, - None => continue, - }; + let uv_aabr = match graphic_cache.queue_res( + *graphic_id, + resolution, + source_aabr, + *rotation, + ) { + Some(aabr) => Aabr { + min: Vec2::new( + (aabr.min.x as f32) / cache_w, + (aabr.max.y as f32) / cache_h, + ), + max: Vec2::new( + (aabr.max.x as f32) / cache_w, + (aabr.min.y as f32) / cache_h, + ), + }, + None => continue, + }; - mesh.push_quad(create_ui_quad(gl_aabr(rect), uv_aabr, color, UiMode::Image)); + mesh.push_quad(create_ui_quad(gl_aabr, uv_aabr, color, UiMode::Image)); } PrimitiveKind::Text { color, diff --git a/voxygen/src/ui/widgets/image_frame.rs b/voxygen/src/ui/widgets/image_frame.rs new file mode 100644 index 0000000000..ea76f82a2e --- /dev/null +++ b/voxygen/src/ui/widgets/image_frame.rs @@ -0,0 +1,319 @@ +//! A widget for selecting a single value along some linear range. +use conrod_core::{ + builder_methods, image, + widget::{self, Image, Rectangle}, + widget_ids, Color, Colorable, Positionable, Rect, Sizeable, UiCell, Widget, WidgetCommon, +}; + +#[derive(Clone, WidgetCommon)] +pub struct ImageFrame { + #[conrod(common_builder)] + common: widget::CommonBuilder, + // Edge images [t, b, r, l] + edges: [image::Id; 4], + edge_src_rects: [Option; 4], + // Corner images [tr, tl, br, bl] + corners: [image::Id; 4], + corner_src_rects: [Option; 4], + // Center + center: Center, + // Thickness of the frame border, determines the size used for the edge and corner images + border_size: BorderSize, + // Color to apply to all images making up the image frame + color: Option, + // TODO: would it be useful to have an optional close button be a part of this? +} + +#[derive(Clone)] +pub enum Center { + Plain(Color), + Image(image::Id, Option), +} +impl From for Center { + fn from(color: Color) -> Self { + Center::Plain(color) + } +} +impl From for Center { + fn from(image: image::Id) -> Self { + Center::Image(image, None) + } +} +impl From<(image::Id, Rect)> for Center { + fn from((image, src_rect): (image::Id, Rect)) -> Self { + Center::Image(image, Some(src_rect)) + } +} + +#[derive(Clone)] +pub struct BorderSize { + top: f64, + bottom: f64, + right: f64, + left: f64, +} +impl From for BorderSize { + fn from(thickness: f64) -> Self { + BorderSize { + top: thickness, + bottom: thickness, + right: thickness, + left: thickness, + } + } +} +impl From<[f64; 2]> for BorderSize { + fn from([vertical, horizontal]: [f64; 2]) -> Self { + BorderSize { + top: horizontal, + bottom: horizontal, + right: vertical, + left: vertical, + } + } +} +impl From<[f64; 4]> for BorderSize { + fn from(vals: [f64; 4]) -> Self { + BorderSize { + top: vals[0], + bottom: vals[1], + right: vals[2], + left: vals[3], + } + } +} + +widget_ids! { + struct Ids { + center_plain, + center_image, + right, + top_right, + top, + top_left, + left, + bottom_left, + bottom, + bottom_right, + } +} + +/// Represents the state of the ImageFrame widget. +pub struct State { + ids: Ids, +} + +impl ImageFrame { + pub fn new( + edges: [image::Id; 4], + corners: [image::Id; 4], + center: impl Into
, + border_size: impl Into, + ) -> Self { + Self { + common: widget::CommonBuilder::default(), + edges, + edge_src_rects: [None; 4], + corners, + corner_src_rects: [None; 4], + center: center.into(), + border_size: border_size.into(), + color: None, + } + } + + builder_methods! { + pub edge_src_rects { edge_src_rects = [Option; 4] } + pub corner_src_rects { corner_src_rects = [Option; 4] } + } +} + +impl Widget for ImageFrame { + type State = State; + type Style = (); + type Event = (); + + fn init_state(&self, id_gen: widget::id::Generator) -> Self::State { + State { + ids: Ids::new(id_gen), + } + } + + fn style(&self) -> Self::Style { + () + } + + /// Update the state of the ImageFrame + fn update(self, args: widget::UpdateArgs) -> Self::Event { + let widget::UpdateArgs { + id, + state, + rect, + ui, + .. + } = args; + + let (frame_w, frame_h) = rect.w_h(); + + let t_height = self.border_size.top.min(frame_h); + let b_height = self.border_size.bottom.min(frame_h); + let r_width = self.border_size.right.min(frame_w); + let l_width = self.border_size.left.min(frame_w); + let inner_width = (frame_w - r_width - l_width).max(0.0); + let inner_height = (frame_h - t_height - b_height).max(0.0); + + let r_rect = Rect::from_xy_dim( + [rect.x() + (inner_width + r_width) / 2.0, rect.y()], + [r_width, inner_height], + ); + let tr_rect = Rect::from_xy_dim( + [ + rect.x() + (inner_width + r_width) / 2.0, + rect.y() + (inner_height + t_height) / 2.0, + ], + [r_width, t_height], + ); + let t_rect = Rect::from_xy_dim( + [rect.x(), rect.y() + (inner_height + t_height) / 2.0], + [inner_width, t_height], + ); + let tl_rect = Rect::from_xy_dim( + [ + rect.x() - (inner_width + l_width) / 2.0, + rect.y() + (inner_height + t_height) / 2.0, + ], + [l_width, t_height], + ); + let l_rect = Rect::from_xy_dim( + [rect.x() - (inner_width + l_width) / 2.0, rect.y()], + [l_width, inner_height], + ); + let bl_rect = Rect::from_xy_dim( + [ + rect.x() - (inner_width + l_width) / 2.0, + rect.y() - (inner_height + b_height) / 2.0, + ], + [l_width, b_height], + ); + let b_rect = Rect::from_xy_dim( + [rect.x(), rect.y() - (inner_height + b_height) / 2.0], + [inner_width, b_height], + ); + let br_rect = Rect::from_xy_dim( + [ + rect.x() + (inner_width + r_width) / 2.0, + rect.y() - (inner_height + b_height) / 2.0, + ], + [r_width, b_height], + ); + + let maybe_color = self.color; + let set_image = |image_id, rect: Rect, maybe_src_rect, widget_id, ui: &mut UiCell| { + Image::new(image_id) + .xy(rect.xy()) + .wh(rect.dim()) + .parent(id) + .graphics_for(id) + .and_then(maybe_src_rect, |w, r| w.source_rectangle(r)) + .color(maybe_color) + .set(widget_id, ui); + }; + // Right edge + set_image( + self.edges[2], + r_rect, + self.edge_src_rects[2], + state.ids.right, + ui, + ); + // Top-right corner + set_image( + self.corners[0], + tr_rect, + self.corner_src_rects[0], + state.ids.top_right, + ui, + ); + // Top edge + set_image( + self.edges[0], + t_rect, + self.edge_src_rects[0], + state.ids.top, + ui, + ); + // Top-left corner + set_image( + self.corners[1], + tl_rect, + self.corner_src_rects[1], + state.ids.top_left, + ui, + ); + // Left edge + set_image( + self.edges[3], + l_rect, + self.edge_src_rects[3], + state.ids.left, + ui, + ); + // Bottom-left corner + set_image( + self.corners[3], + bl_rect, + self.corner_src_rects[3], + state.ids.bottom_left, + ui, + ); + // Bottom edge + set_image( + self.edges[1], + b_rect, + self.edge_src_rects[1], + state.ids.bottom, + ui, + ); + // Bottom-right corner + set_image( + self.corners[2], + br_rect, + self.corner_src_rects[2], + state.ids.bottom_right, + ui, + ); + + // Center, + match self.center { + Center::Plain(color) => { + Rectangle::fill_with([inner_width, inner_height], color) + .xy(rect.xy()) + .parent(id) + .graphics_for(id) + .and_then(maybe_color, |w, c| { + w.color(color.alpha(match c { + Color::Rgba(_, _, _, a) => a, + Color::Hsla(_, _, _, a) => a, + })) + }) + .set(state.ids.center_plain, ui); + } + Center::Image(image_id, maybe_src_rect) => { + set_image( + image_id, + Rect::from_xy_dim(rect.xy(), [inner_width, inner_height]), + maybe_src_rect, + state.ids.center_image, + ui, + ); + } + } + } +} + +impl Colorable for ImageFrame { + fn color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } +} diff --git a/voxygen/src/ui/widgets/image_slider.rs b/voxygen/src/ui/widgets/image_slider.rs index daa33d9721..252e780262 100644 --- a/voxygen/src/ui/widgets/image_slider.rs +++ b/voxygen/src/ui/widgets/image_slider.rs @@ -67,7 +67,7 @@ widget_ids! { } } -/// Represents the state of the Slider widget. +/// Represents the state of the ImageSlider widget. pub struct State { ids: Ids, } diff --git a/voxygen/src/ui/widgets/mod.rs b/voxygen/src/ui/widgets/mod.rs index a3987f1a94..3cdc003505 100644 --- a/voxygen/src/ui/widgets/mod.rs +++ b/voxygen/src/ui/widgets/mod.rs @@ -1,3 +1,4 @@ +pub mod image_frame; pub mod image_slider; pub mod ingame; pub mod toggle_button; diff --git a/voxygen/src/ui/widgets/tooltip.rs b/voxygen/src/ui/widgets/tooltip.rs index 81a7240b77..dc2219c3c1 100644 --- a/voxygen/src/ui/widgets/tooltip.rs +++ b/voxygen/src/ui/widgets/tooltip.rs @@ -1,6 +1,8 @@ +use super::image_frame::ImageFrame; use conrod_core::{ - builder_method, builder_methods, input::global::Global, text, widget, widget_ids, Color, - Colorable, FontSize, Positionable, Sizeable, UiCell, Widget, WidgetCommon, WidgetStyle, + builder_method, builder_methods, image, input::global::Global, position::Dimension, text, + widget, widget_ids, Color, Colorable, FontSize, Positionable, Sizeable, Ui, UiCell, Widget, + WidgetCommon, WidgetStyle, }; use std::time::{Duration, Instant}; @@ -87,11 +89,28 @@ impl TooltipManager { } } } - fn set_tooltip(&mut self, tooltip: Tooltip, src_id: widget::Id, ui: &mut UiCell) { + fn set_tooltip( + &mut self, + tooltip: &Tooltip, + title_text: &str, + desc_text: &str, + img_id: Option, + image_dims: Option<(f64, f64)>, + src_id: widget::Id, + ui: &mut UiCell, + ) { let tooltip_id = self.tooltip_id; let mp_h = MOUSE_PAD_Y / self.logical_scale_factor; let tooltip = |transparency, mouse_pos: [f64; 2], ui: &mut UiCell| { + // Fill in text and the potential image beforehand to get an accurate size for spacing + let tooltip = tooltip + .clone() + .title(title_text) + .desc(desc_text) + .image(img_id) + .image_dims(image_dims); + let [t_w, t_h] = tooltip.get_wh(ui).unwrap_or([0.0, 0.0]); let [m_x, m_y] = mouse_pos; let (w_w, w_h) = (ui.win_w, ui.win_h); @@ -128,12 +147,32 @@ impl TooltipManager { pub struct Tooltipped<'a, W> { inner: W, tooltip_manager: &'a mut TooltipManager, - tooltip: Tooltip<'a>, + title_text: &'a str, + desc_text: &'a str, + img_id: Option, + image_dims: Option<(f64, f64)>, + tooltip: &'a Tooltip<'a>, } impl<'a, W: Widget> Tooltipped<'a, W> { + pub fn tooltip_image(mut self, img_id: image::Id) -> Self { + self.img_id = Some(img_id); + self + } + pub fn tooltip_image_dims(mut self, dims: (f64, f64)) -> Self { + self.image_dims = Some(dims); + self + } pub fn set(self, id: widget::Id, ui: &mut UiCell) -> W::Event { let event = self.inner.set(id, ui); - self.tooltip_manager.set_tooltip(self.tooltip, id, ui); + self.tooltip_manager.set_tooltip( + self.tooltip, + self.title_text, + self.desc_text, + self.img_id, + self.image_dims, + id, + ui, + ); event } } @@ -143,7 +182,9 @@ pub trait Tooltipable { fn with_tooltip<'a>( self, tooltip_manager: &'a mut TooltipManager, - tooltip: Tooltip<'a>, + title_text: &'a str, + desc_text: &'a str, + tooltip: &'a Tooltip<'a>, ) -> Tooltipped<'a, Self> where Self: std::marker::Sized; @@ -152,16 +193,33 @@ impl Tooltipable for W { fn with_tooltip<'a>( self, tooltip_manager: &'a mut TooltipManager, - tooltip: Tooltip<'a>, + title_text: &'a str, + desc_text: &'a str, + tooltip: &'a Tooltip<'a>, ) -> Tooltipped<'a, W> { Tooltipped { inner: self, tooltip_manager, + title_text, + desc_text, + img_id: None, + image_dims: None, tooltip, } } } +/// Vertical spacing between elements of the tooltip +const V_PAD: f64 = 10.0; +/// Horizontal spacing between elements of the tooltip +const H_PAD: f64 = 10.0; +/// Default portion of inner width that goes to an image +const IMAGE_W_FRAC: f64 = 0.3; +/// Default width multiplied by the description font size +const DEFAULT_CHAR_W: f64 = 30.0; +/// Text vertical spacing factor to account for overhanging text +const TEXT_SPACE_FACTOR: f64 = 0.35; + /// A widget for displaying tooltips #[derive(Clone, WidgetCommon)] pub struct Tooltip<'a> { @@ -169,13 +227,16 @@ pub struct Tooltip<'a> { common: widget::CommonBuilder, title_text: &'a str, desc_text: &'a str, + image: Option, + image_dims: Option<(f64, f64)>, style: Style, transparency: f32, + image_frame: ImageFrame, } #[derive(Clone, Debug, Default, PartialEq, WidgetStyle)] pub struct Style { - #[conrod(default = "theme.background_color")] + #[conrod(default = "Color::Rgba(1.0, 1.0, 1.0, 1.0)")] pub color: Option, title: widget::text::Style, desc: widget::text::Style, @@ -186,7 +247,8 @@ widget_ids! { struct Ids { title, desc, - back_rect, + image_frame, + image, } } @@ -195,13 +257,16 @@ pub struct State { } impl<'a> Tooltip<'a> { - pub fn new(title: &'a str, desc: &'a str) -> Self { + pub fn new(image_frame: ImageFrame) -> Self { Tooltip { common: widget::CommonBuilder::default(), style: Style::default(), - title_text: title, - desc_text: desc, + title_text: "", + desc_text: "", transparency: 1.0, + image_frame, + image: None, + image_dims: None, } } @@ -220,7 +285,28 @@ impl<'a> Tooltip<'a> { // self.justify(text::Justify::Right) //} - // TODO: add method(s) to make children widgets and use that to determine height in height function (and in update to draw the widgets) + fn text_image_width(&self, total_width: f64) -> (f64, f64) { + let inner_width = (total_width - H_PAD * 2.0).max(0.0); + // Image defaults to 30% of the width + let image_w = if self.image.is_some() { + match self.image_dims { + Some((w, _)) => w, + None => (inner_width - H_PAD).max(0.0) * IMAGE_W_FRAC, + } + } else { + 0.0 + }; + // Text gets the remaining width + let text_w = (inner_width + - if self.image.is_some() { + image_w + H_PAD + } else { + 0.0 + }) + .max(0.0); + + (text_w, image_w) + } /// Specify the font used for displaying the text. pub fn font_id(mut self, font_id: text::font::Id) -> Self { @@ -236,6 +322,10 @@ impl<'a> Tooltip<'a> { pub desc_font_size { style.desc.font_size = Some(FontSize) } pub title_justify { style.title.justify = Some(text::Justify) } pub desc_justify { style.desc.justify = Some(text::Justify) } + image { image = Option } + title { title_text = &'a str } + desc { desc_text = &'a str } + image_dims { image_dims = Option<(f64, f64)> } transparency { transparency = f32 } } } @@ -265,41 +355,132 @@ impl<'a> Widget for Tooltip<'a> { .. } = args; + // Widths + let (text_w, image_w) = self.text_image_width(rect.w()); + // Apply transparency let color = style.color(ui.theme()).alpha(self.transparency); - // Background rectangle - widget::Rectangle::fill(rect.dim()) + // Background image frame + self.image_frame + .wh(rect.dim()) .xy(rect.xy()) .graphics_for(id) .parent(id) .color(color) - //.floating(true) - .set(state.ids.back_rect, ui); + .set(state.ids.image_frame, ui); + + // Image + if let Some(img_id) = self.image { + widget::Image::new(img_id) + .w_h(image_w, self.image_dims.map_or(image_w, |(_, h)| h)) + .graphics_for(id) + .parent(id) + .color(Some(color)) + .top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + .set(state.ids.image, ui); + } + + // Spacing for overhanging text + let title_space = self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR; // Title of tooltip - widget::Text::new(self.title_text) - .w(rect.w()) - .graphics_for(id) - .parent(id) - .top_left_with_margins_on(state.ids.back_rect, 5.0, 5.0) - .with_style(self.style.title) - // Apply transparency - .color(style.title.color(ui.theme()).alpha(self.transparency)) - //.floating(true) + if !self.title_text.is_empty() { + let title = widget::Text::new(self.title_text) + .w(text_w) + .graphics_for(id) + .parent(id) + .with_style(self.style.title) + // Apply transparency + .color(style.title.color(ui.theme()).alpha(self.transparency)); + + if self.image.is_some() { + title + .right_from(state.ids.image, H_PAD) + .align_top_of(state.ids.image) + } else { + title.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + } .set(state.ids.title, ui); + } // Description of tooltip - widget::Text::new(self.desc_text) - .w(rect.w()) + let desc = widget::Text::new(self.desc_text) + .w(text_w) .graphics_for(id) .parent(id) - .down_from(state.ids.title, 10.0) - .with_style(self.style.desc) // Apply transparency .color(style.desc.color(ui.theme()).alpha(self.transparency)) - // .floating(true) - .set(state.ids.desc, ui); + .with_style(self.style.desc); + + if !self.title_text.is_empty() { + desc.down_from(state.ids.title, V_PAD * 0.5 + title_space) + .align_left_of(state.ids.title) + } else { + if self.image.is_some() { + desc.right_from(state.ids.image, H_PAD) + .align_top_of(state.ids.image) + } else { + desc.top_left_with_margins_on(state.ids.image_frame, V_PAD, H_PAD) + } + } + .set(state.ids.desc, ui); + } + + /// Default width is based on the description font size unless the text is small enough to fit on a single line + fn default_x_dimension(&self, ui: &Ui) -> Dimension { + let single_line_title_w = widget::Text::new(self.title_text) + .with_style(self.style.title) + .get_w(ui) + .unwrap_or(0.0); + let single_line_desc_w = widget::Text::new(self.desc_text) + .with_style(self.style.desc) + .get_w(ui) + .unwrap_or(0.0); + + let text_w = single_line_title_w.max(single_line_desc_w); + let inner_w = if self.image.is_some() { + match self.image_dims { + Some((w, _)) => w + text_w + H_PAD, + None => text_w / (1.0 - IMAGE_W_FRAC) + H_PAD, + } + } else { + text_w + }; + + let width = + inner_w.min(self.style.desc.font_size(&ui.theme) as f64 * DEFAULT_CHAR_W) + 2.0 * H_PAD; + Dimension::Absolute(width) + } + + fn default_y_dimension(&self, ui: &Ui) -> Dimension { + let (text_w, image_w) = self.text_image_width(self.get_w(ui).unwrap_or(0.0)); + let title_h = if self.title_text.is_empty() { + 0.0 + } else { + widget::Text::new(self.title_text) + .with_style(self.style.title) + .w(text_w) + .get_h(ui) + .unwrap_or(0.0) + + self.style.title.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR + + 0.5 * V_PAD + }; + let desc_h = if self.desc_text.is_empty() { + 0.0 + } else { + widget::Text::new(self.desc_text) + .with_style(self.style.desc) + .w(text_w) + .get_h(ui) + .unwrap_or(0.0) + + self.style.desc.font_size(&ui.theme) as f64 * TEXT_SPACE_FACTOR + }; + // Image defaults to square shape + let image_h = self.image_dims.map_or(image_w, |(_, h)| h); + // Title height + desc height + padding/spacing + let height = (title_h + desc_h).max(image_h) + 2.0 * V_PAD; + Dimension::Absolute(height) } }