mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
e40702c083
Removes the Scale::physical_resolution method as it could become desynced from the renderer resolution causing the panic
1050 lines
45 KiB
Rust
1050 lines
45 KiB
Rust
mod cache;
|
|
mod event;
|
|
mod graphic;
|
|
mod scale;
|
|
mod widgets;
|
|
#[macro_use]
|
|
pub mod img_ids;
|
|
#[macro_use]
|
|
pub mod fonts;
|
|
pub mod ice;
|
|
|
|
pub use event::Event;
|
|
pub use graphic::{Graphic, Id as GraphicId, Rotation, SampleStrat, Transform};
|
|
pub use scale::{Scale, ScaleMode};
|
|
pub use widgets::{
|
|
image_frame::ImageFrame,
|
|
image_slider::ImageSlider,
|
|
ingame::{Ingame, Ingameable},
|
|
item_tooltip::{ItemTooltip, ItemTooltipManager, ItemTooltipable},
|
|
radio_list::RadioList,
|
|
slot,
|
|
toggle_button::ToggleButton,
|
|
tooltip::{Tooltip, TooltipManager, Tooltipable},
|
|
};
|
|
|
|
use crate::{
|
|
render::{
|
|
create_ui_quad, create_ui_tri, Consts, DynamicModel, Globals, Mesh, RenderError, Renderer,
|
|
UiBoundLocals, UiDrawer, UiLocals, UiMode, UiVertex,
|
|
},
|
|
window::Window,
|
|
Error,
|
|
};
|
|
#[rustfmt::skip]
|
|
use ::image::GenericImageView;
|
|
use cache::Cache;
|
|
use common::util::srgba_to_linear;
|
|
use common_base::span;
|
|
use conrod_core::{
|
|
event::Input,
|
|
graph::{self, Graph},
|
|
image::{Id as ImageId, Map},
|
|
input::{touch::Touch, Motion, Widget},
|
|
render::{Primitive, PrimitiveKind},
|
|
text::{self, font},
|
|
widget::{self, id::Generator},
|
|
Rect, Scalar, UiBuilder, UiCell,
|
|
};
|
|
use core::{convert::TryInto, f32, f64, ops::Range};
|
|
use graphic::TexId;
|
|
use hashbrown::hash_map::Entry;
|
|
use std::time::Duration;
|
|
use tracing::{error, warn};
|
|
use vek::*;
|
|
|
|
#[derive(Debug)]
|
|
pub enum UiError {
|
|
RenderError(RenderError),
|
|
}
|
|
|
|
enum DrawKind {
|
|
Image(TexId),
|
|
// Text and non-textured geometry
|
|
Plain,
|
|
}
|
|
|
|
enum DrawCommand {
|
|
Draw { kind: DrawKind, verts: Range<u32> },
|
|
Scissor(Aabr<u16>),
|
|
WorldPos(Option<usize>),
|
|
}
|
|
impl DrawCommand {
|
|
fn image(verts: Range<usize>, id: TexId) -> DrawCommand {
|
|
DrawCommand::Draw {
|
|
kind: DrawKind::Image(id),
|
|
verts: verts
|
|
.start
|
|
.try_into()
|
|
.expect("Vertex count for UI rendering does not fit in a u32!")
|
|
..verts
|
|
.end
|
|
.try_into()
|
|
.expect("Vertex count for UI rendering does not fit in a u32!"),
|
|
}
|
|
}
|
|
|
|
fn plain(verts: Range<usize>) -> DrawCommand {
|
|
DrawCommand::Draw {
|
|
kind: DrawKind::Plain,
|
|
verts: verts
|
|
.start
|
|
.try_into()
|
|
.expect("Vertex count for UI rendering does not fit in a u32!")
|
|
..verts
|
|
.end
|
|
.try_into()
|
|
.expect("Vertex count for UI rendering does not fit in a u32!"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Ui {
|
|
pub ui: conrod_core::Ui,
|
|
image_map: Map<(graphic::Id, Rotation)>,
|
|
cache: Cache,
|
|
// Draw commands for the next render
|
|
draw_commands: Vec<DrawCommand>,
|
|
// Mesh buffer for UI vertices; we reuse its allocation in order to limit vector reallocations
|
|
// during redrawing.
|
|
mesh: Mesh<UiVertex>,
|
|
// Model for drawing the ui
|
|
model: DynamicModel<UiVertex>,
|
|
// Consts for default ui drawing position (ie the interface)
|
|
interface_locals: UiBoundLocals,
|
|
// Consts to specify positions of ingame elements (e.g. Nametags)
|
|
ingame_locals: Vec<UiBoundLocals>,
|
|
// Window size for updating scaling
|
|
window_resized: Option<Vec2<f64>>,
|
|
// Scale factor changed
|
|
scale_factor_changed: Option<f64>,
|
|
// Used to delay cache resizing until after current frame is drawn
|
|
need_cache_resize: bool,
|
|
// Scaling of the ui
|
|
scale: Scale,
|
|
// Tooltips
|
|
tooltip_manager: TooltipManager,
|
|
// Item tooltips manager
|
|
item_tooltip_manager: ItemTooltipManager,
|
|
// Scissor for the whole window
|
|
window_scissor: Aabr<u16>,
|
|
}
|
|
|
|
impl Ui {
|
|
pub fn new(window: &mut Window) -> Result<Self, Error> {
|
|
let scale = Scale::new(window, ScaleMode::Absolute(1.0), 1.0);
|
|
let win_dims = scale.scaled_resolution().into_array();
|
|
|
|
let renderer = window.renderer_mut();
|
|
|
|
let physical_resolution = renderer.resolution();
|
|
|
|
let mut ui = UiBuilder::new(win_dims).build();
|
|
// NOTE: Since we redraw the actual frame each time whether or not the UI needs
|
|
// to be updated, there's no reason to set the redraw count higher than
|
|
// 1.
|
|
ui.set_num_redraw_frames(1);
|
|
|
|
let item_tooltip_manager = ItemTooltipManager::new(
|
|
ui.widget_id_generator(),
|
|
Duration::from_millis(1),
|
|
Duration::from_millis(0),
|
|
scale.scale_factor_logical(),
|
|
);
|
|
|
|
let tooltip_manager = TooltipManager::new(
|
|
ui.widget_id_generator(),
|
|
Duration::from_millis(1),
|
|
Duration::from_millis(0),
|
|
scale.scale_factor_logical(),
|
|
);
|
|
|
|
let interface_locals = renderer.create_ui_bound_locals(&[UiLocals::default()]);
|
|
|
|
Ok(Self {
|
|
ui,
|
|
image_map: Map::new(),
|
|
cache: Cache::new(renderer)?,
|
|
draw_commands: Vec::new(),
|
|
mesh: Mesh::new(),
|
|
model: renderer.create_dynamic_model(100),
|
|
interface_locals,
|
|
ingame_locals: Vec::new(),
|
|
window_resized: None,
|
|
scale_factor_changed: None,
|
|
need_cache_resize: false,
|
|
scale,
|
|
tooltip_manager,
|
|
item_tooltip_manager,
|
|
window_scissor: default_scissor(physical_resolution),
|
|
})
|
|
}
|
|
|
|
// Set the scaling mode of the ui.
|
|
pub fn set_scaling_mode(&mut self, mode: ScaleMode) {
|
|
self.scale.set_scaling_mode(mode);
|
|
// To clear the cache (it won't be resized in this case)
|
|
self.need_cache_resize = true;
|
|
// Give conrod the new size.
|
|
let (w, h) = self.scale.scaled_resolution().into_tuple();
|
|
self.ui.handle_event(Input::Resize(w, h));
|
|
}
|
|
|
|
pub fn scale_factor_changed(&mut self, scale_factor: f64) {
|
|
self.scale_factor_changed = Some(scale_factor);
|
|
}
|
|
|
|
// Get a copy of Scale
|
|
pub fn scale(&self) -> Scale { self.scale }
|
|
|
|
pub fn add_graphic(&mut self, graphic: Graphic) -> ImageId {
|
|
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)),
|
|
// Hacky way to make sure a source rectangle always faces north regardless of player
|
|
// orientation.
|
|
// This is an easy way to get around Conrod's lack of rotation data for images (for this
|
|
// specific use case).
|
|
source_north: self.image_map.insert((graphic_id, Rotation::SourceNorth)),
|
|
// Hacky way to make sure a target rectangle always faces north regardless of player
|
|
// orientation.
|
|
// This is an easy way to get around Conrod's lack of rotation data for images (for this
|
|
// specific use case).
|
|
target_north: self.image_map.insert((graphic_id, Rotation::TargetNorth)),
|
|
}
|
|
}
|
|
|
|
pub fn replace_graphic(&mut self, id: ImageId, graphic: Graphic) {
|
|
let graphic_id = if let Some((graphic_id, _)) = self.image_map.get(&id) {
|
|
*graphic_id
|
|
} else {
|
|
error!("Failed to replace graphic the provided id is not in use");
|
|
return;
|
|
};
|
|
self.cache.replace_graphic(graphic_id, graphic);
|
|
self.image_map.replace(id, (graphic_id, Rotation::None));
|
|
}
|
|
|
|
pub fn new_font(&mut self, font: crate::ui::ice::RawFont) -> font::Id {
|
|
let font = text::Font::from_bytes(font.0).unwrap();
|
|
self.ui.fonts.insert(font)
|
|
}
|
|
|
|
pub fn id_generator(&mut self) -> Generator { self.ui.widget_id_generator() }
|
|
|
|
pub fn set_widgets(&mut self) -> (UiCell, &mut ItemTooltipManager, &mut TooltipManager) {
|
|
(
|
|
self.ui.set_widgets(),
|
|
&mut self.item_tooltip_manager,
|
|
&mut self.tooltip_manager,
|
|
)
|
|
}
|
|
|
|
pub fn set_item_widgets(&mut self) -> (UiCell, &mut ItemTooltipManager) {
|
|
(self.ui.set_widgets(), &mut self.item_tooltip_manager)
|
|
}
|
|
|
|
// Accepts Option so widget can be unfocused.
|
|
pub fn focus_widget(&mut self, id: Option<widget::Id>) {
|
|
self.ui.keyboard_capture(match id {
|
|
Some(id) => id,
|
|
None => self.ui.window,
|
|
});
|
|
}
|
|
|
|
// Get id of current widget capturing keyboard.
|
|
pub fn widget_capturing_keyboard(&self) -> Option<widget::Id> {
|
|
self.ui.global_input().current.widget_capturing_keyboard
|
|
}
|
|
|
|
// Get whether a widget besides the window is capturing the mouse.
|
|
pub fn no_widget_capturing_mouse(&self) -> bool {
|
|
self.ui
|
|
.global_input()
|
|
.current
|
|
.widget_capturing_mouse
|
|
.filter(|id| id != &self.ui.window)
|
|
.is_none()
|
|
}
|
|
|
|
// Get the widget graph.
|
|
pub fn widget_graph(&self) -> &Graph { self.ui.widget_graph() }
|
|
|
|
pub fn handle_event(&mut self, event: Event) {
|
|
match event.0 {
|
|
Input::Resize(w, h) => {
|
|
if w > 1.0 && h > 1.0 {
|
|
self.window_resized = Some(Vec2::new(w, h))
|
|
}
|
|
},
|
|
Input::Touch(touch) => self.ui.handle_event(Input::Touch(Touch {
|
|
xy: self.scale.scale_point(touch.xy.into()).into_array(),
|
|
..touch
|
|
})),
|
|
Input::Motion(motion) => self.ui.handle_event(Input::Motion(match motion {
|
|
Motion::MouseCursor { x, y } => {
|
|
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
|
|
Motion::MouseCursor { x, y }
|
|
},
|
|
Motion::MouseRelative { x, y } => {
|
|
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
|
|
Motion::MouseRelative { x, y }
|
|
},
|
|
Motion::Scroll { x, y } => {
|
|
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
|
|
Motion::Scroll { x, y }
|
|
},
|
|
_ => motion,
|
|
})),
|
|
_ => self.ui.handle_event(event.0),
|
|
}
|
|
}
|
|
|
|
pub fn widget_input(&self, id: widget::Id) -> Widget { self.ui.widget_input(id) }
|
|
|
|
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
|
|
pub fn maintain(&mut self, renderer: &mut Renderer, view_projection_mat: Option<Mat4<f32>>) {
|
|
span!(_guard, "maintain", "Ui::maintain");
|
|
// Maintain tooltip manager
|
|
self.tooltip_manager
|
|
.maintain(self.ui.global_input(), self.scale.scale_factor_logical());
|
|
|
|
// Maintain tooltip manager
|
|
self.item_tooltip_manager
|
|
.maintain(self.ui.global_input(), self.scale.scale_factor_logical());
|
|
|
|
// Handle scale factor changing
|
|
let need_resize = if let Some(scale_factor) = self.scale_factor_changed.take() {
|
|
self.scale.scale_factor_changed(scale_factor)
|
|
} else {
|
|
false
|
|
};
|
|
|
|
// Handle window resizing.
|
|
let need_resize = if let Some(new_dims) = self.window_resized.take() {
|
|
let (old_w, old_h) = self.scale.scaled_resolution().into_tuple();
|
|
self.scale.window_resized(new_dims);
|
|
let (w, h) = self.scale.scaled_resolution().into_tuple();
|
|
self.ui.handle_event(Input::Resize(w, h));
|
|
self.window_scissor = default_scissor(renderer.resolution());
|
|
|
|
// Avoid panic in graphic cache when minimizing.
|
|
// Avoid resetting cache if window size didn't change
|
|
// Somewhat inefficient for elements that won't change size after a window
|
|
// resize
|
|
let res = renderer.resolution();
|
|
res.x > 0 && res.y > 0 && !(old_w == w && old_h == h)
|
|
} else {
|
|
false
|
|
} || need_resize;
|
|
|
|
if need_resize {
|
|
self.need_cache_resize = true;
|
|
}
|
|
|
|
if self.need_cache_resize {
|
|
// Resize graphic cache
|
|
// FIXME: Handle errors here.
|
|
self.cache.resize(renderer).unwrap();
|
|
|
|
self.need_cache_resize = false;
|
|
}
|
|
|
|
let mut retry = false;
|
|
self.maintain_internal(renderer, view_projection_mat, &mut retry);
|
|
if retry {
|
|
// Update the glyph cache and try again.
|
|
self.maintain_internal(renderer, view_projection_mat, &mut retry);
|
|
}
|
|
}
|
|
|
|
fn maintain_internal(
|
|
&mut self,
|
|
renderer: &mut Renderer,
|
|
view_projection_mat: Option<Mat4<f32>>,
|
|
retry: &mut bool,
|
|
) {
|
|
span!(_guard, "internal", "Ui::maintain_internal");
|
|
let (graphic_cache, text_cache, glyph_cache, cache_tex) = self.cache.cache_mut_and_tex();
|
|
|
|
let mut primitives = if *retry {
|
|
// If this is a retry, always redraw.
|
|
self.ui.draw()
|
|
} else {
|
|
// Otherwise, redraw only if widgets were actually updated.
|
|
match self.ui.draw_if_changed() {
|
|
Some(primitives) => primitives,
|
|
None => return,
|
|
}
|
|
};
|
|
|
|
let (half_res, x_align, y_align) = {
|
|
let res = renderer.resolution();
|
|
(
|
|
res.map(|e| e as f32 / 2.0),
|
|
(res.x & 1) as f32 * 0.5,
|
|
(res.y & 1) as f32 * 0.5,
|
|
)
|
|
};
|
|
|
|
let ui = &self.ui;
|
|
let p_scale_factor = self.scale.scale_factor_physical();
|
|
// Functions for converting for conrod scalar coords to GL vertex coords (-1.0
|
|
// to 1.0).
|
|
let (ui_win_w, ui_win_h) = (ui.win_w, ui.win_h);
|
|
let vx = |x: f64| (x / ui_win_w * 2.0) as f32;
|
|
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();
|
|
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 }
|
|
};
|
|
|
|
// let window_dim = ui.window_dim();
|
|
let theme = &ui.theme;
|
|
let widget_graph = ui.widget_graph();
|
|
let fonts = &ui.fonts;
|
|
let dpi_factor = p_scale_factor as f32;
|
|
|
|
// We can use information about whether a widget was actually updated to more
|
|
// easily track cache invalidations.
|
|
let updated_widgets = ui.updated_widgets();
|
|
let mut glyph_missing = false;
|
|
|
|
updated_widgets.iter()
|
|
// Filter out widgets that are either:
|
|
// - not text primitives, or
|
|
// - are text primitives, but were both already in the cache, and not updated this
|
|
// frame.
|
|
//
|
|
// The reason the second condition is so complicated is that we want to handle cases
|
|
// where we cleared the whole cache, which can result in glyphs from text updated in a
|
|
// previous frame not being present in the text cache.
|
|
.filter_map(|(&widget_id, updated)| {
|
|
widget_graph.widget(widget_id)
|
|
.and_then(|widget| Some((widget.rect, widget.unique_widget_state::<widget::Text>()?)))
|
|
.and_then(|(rect, text)| {
|
|
// NOTE: This fallback is weird and probably shouldn't exist.
|
|
let font_id = text.style.font_id(theme)/*.or_else(|| fonts.ids().next())*/?;
|
|
let font = fonts.get(font_id)?;
|
|
|
|
Some((widget_id, updated, rect, text, font_id, font))
|
|
})
|
|
})
|
|
// Recache the entry.
|
|
.for_each(|(widget_id, updated, rect, graph::UniqueWidgetState { state, style }, font_id, font)| {
|
|
let entry = match text_cache.entry(widget_id) {
|
|
Entry::Occupied(_) if !updated => return,
|
|
entry => entry,
|
|
};
|
|
|
|
// Retrieve styling.
|
|
let color = style.color(theme);
|
|
let font_size = style.font_size(theme);
|
|
let line_spacing = style.line_spacing(theme);
|
|
let justify = style.justify(theme);
|
|
let y_align = conrod_core::position::Align::End;
|
|
|
|
let text = &state.string;
|
|
let line_infos = &state.line_infos;
|
|
|
|
// Convert conrod coordinates to pixel coordinates.
|
|
let trans_x = |x: Scalar| (x + ui_win_w / 2.0) * dpi_factor as Scalar;
|
|
let trans_y = |y: Scalar| ((-y) + ui_win_h / 2.0) * dpi_factor as Scalar;
|
|
|
|
// Produce the text layout iterators.
|
|
let lines = line_infos.iter().map(|info| &text[info.byte_range()]);
|
|
let line_rects = text::line::rects(line_infos.iter(), font_size, rect,
|
|
justify, y_align, line_spacing);
|
|
|
|
// Grab the positioned glyphs from the text primitives.
|
|
let scale = text::f32_pt_to_scale(font_size as f32 * dpi_factor);
|
|
let positioned_glyphs = lines.zip(line_rects).flat_map(|(line, line_rect)| {
|
|
let (x, y) = (trans_x(line_rect.left()) as f32, trans_y(line_rect.bottom()) as f32);
|
|
let point = text::rt::Point { x, y };
|
|
font.layout(line, scale, point)
|
|
});
|
|
|
|
// Reuse the mesh allocation if possible at this entry if possible; we
|
|
// then clear it to avoid using stale entries.
|
|
let mesh = entry.or_insert_with(Mesh::new);
|
|
mesh.clear();
|
|
|
|
let color = srgba_to_linear(color.to_fsa().into());
|
|
|
|
positioned_glyphs.for_each(|g| {
|
|
match glyph_cache.rect_for(font_id.index(), &g) {
|
|
Ok(Some((uv_rect, screen_rect))) => {
|
|
let uv = Aabr {
|
|
min: Vec2::new(uv_rect.min.x, uv_rect.max.y),
|
|
max: Vec2::new(uv_rect.max.x, uv_rect.min.y),
|
|
};
|
|
let rect = Aabr {
|
|
min: Vec2::new(
|
|
vx(screen_rect.min.x as f64 / p_scale_factor
|
|
- ui.win_w / 2.0),
|
|
vy(ui.win_h / 2.0
|
|
- screen_rect.max.y as f64 / p_scale_factor),
|
|
),
|
|
max: Vec2::new(
|
|
vx(screen_rect.max.x as f64 / p_scale_factor
|
|
- ui.win_w / 2.0),
|
|
vy(ui.win_h / 2.0
|
|
- screen_rect.min.y as f64 / p_scale_factor),
|
|
),
|
|
};
|
|
mesh.push_quad(create_ui_quad(rect, uv, color, UiMode::Text));
|
|
},
|
|
// Glyph not found, no-op.
|
|
Ok(None) => {},
|
|
// Glyph was found, but was not yet cached; we'll need to add it to the
|
|
// cache and retry.
|
|
Err(_) => {
|
|
// Queue the unknown glyph to be cached.
|
|
glyph_missing = true;
|
|
}
|
|
}
|
|
|
|
// NOTE: Important to do this for *all* glyphs to try to make sure that
|
|
// any that are uncached are part of the graph. Because we always
|
|
// clear the entire cache whenever a new glyph is encountered, by
|
|
// adding and checking all glyphs as they come in we guarantee that (1)
|
|
// any glyphs in the text cache are in the queue, and (2) any glyphs
|
|
// not in the text cache are either in the glyph cache, or (during our
|
|
// processing here) we set the retry flag to force a glyph cache
|
|
// update. Setting the flag causes all glyphs in the current queue to
|
|
// become part of the glyph cache during the second call to
|
|
// `maintain_internal`, so as long as the cache refresh succeeded,
|
|
// during the second call all glyphs will hit this branch as desired.
|
|
glyph_cache.queue_glyph(font_id.index(), g);
|
|
});
|
|
});
|
|
|
|
if glyph_missing {
|
|
if *retry {
|
|
// If a glyph was missing and this was our second try, we know something was
|
|
// messed up during the glyph_cache redraw. It is possible that
|
|
// the queue contained unneeded glyphs, so we don't necessarily
|
|
// have to give up; a more precise enumeration of the
|
|
// glyphs required to render this frame might work out. However, this is a
|
|
// pretty remote edge case, so we opt to not care about this
|
|
// frame (we skip rendering it, basically), and just clear the
|
|
// text cache and glyph queue; next frame will then
|
|
// start out with an empty slate, and therefore will enqueue precisely the
|
|
// glyphs needed for that frame. If *that* frame fails, we're
|
|
// in bigger trouble.
|
|
text_cache.clear();
|
|
glyph_cache.clear();
|
|
glyph_cache.clear_queue();
|
|
self.ui.needs_redraw();
|
|
warn!("Could not recache queued glyphs, skipping frame.");
|
|
} else {
|
|
// NOTE: If this is the first round after encountering a new glyph, we just
|
|
// refresh the whole glyph cache. Technically this is not necessary since
|
|
// positioned_glyphs should still be accurate, but it's just the easiest way
|
|
// to ensure that all glyph positions get updated. It also helps keep the glyph
|
|
// cache reasonable by making sure any glyphs that subsequently get rendered are
|
|
// actually in the cache, including glyphs that were mapped to ids but didn't
|
|
// happen to be rendered on the frame where the cache was
|
|
// refreshed.
|
|
text_cache.clear();
|
|
tracing::debug!("Updating glyphs and clearing text cache.");
|
|
|
|
if let Err(err) = glyph_cache.cache_queued(|rect, data| {
|
|
let offset = [rect.min.x as u32, rect.min.y as u32];
|
|
let size = [rect.width() as u32, rect.height() as u32];
|
|
|
|
let new_data = data
|
|
.iter()
|
|
.map(|x| [255, 255, 255, *x])
|
|
.collect::<Vec<[u8; 4]>>();
|
|
|
|
renderer.update_texture(&cache_tex.0, offset, size, &new_data);
|
|
}) {
|
|
// FIXME: If we actually hit this error, it's still possible we could salvage
|
|
// things in various ways (for instance, the current queue might have extra
|
|
// stuff in it, so we could try calling maintain_internal a
|
|
// third time with a fully clean queue; or we could try to
|
|
// increase the glyph texture size, etc. But hopefully
|
|
// we will not actually encounter this error.
|
|
warn!("Failed to cache queued glyphs: {:?}", err);
|
|
|
|
// Clear queued glyphs, so that (hopefully) next time we won't have the
|
|
// offending glyph or glyph set. We then exit the loop and don't try to
|
|
// rerender the frame.
|
|
glyph_cache.clear_queue();
|
|
self.ui.needs_redraw();
|
|
} else {
|
|
// Successfully cached, so repeat the loop.
|
|
*retry = true;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
self.draw_commands.clear();
|
|
let mesh = &mut self.mesh;
|
|
mesh.clear();
|
|
|
|
enum State {
|
|
Image(TexId),
|
|
Plain,
|
|
}
|
|
|
|
let mut current_state = State::Plain;
|
|
let mut start = 0;
|
|
|
|
let window_scissor = self.window_scissor;
|
|
let mut current_scissor = window_scissor;
|
|
|
|
let mut ingame_local_index = 0;
|
|
|
|
enum Placement {
|
|
Interface,
|
|
// Number of primitives left to render ingame and visibility
|
|
InWorld(usize, bool),
|
|
}
|
|
|
|
let mut placement = Placement::Interface;
|
|
|
|
// Switches to the `Plain` state and completes the previous `Command` if not
|
|
// already in the `Plain` state.
|
|
macro_rules! switch_to_plain_state {
|
|
() => {
|
|
if let State::Image(id) = current_state {
|
|
self.draw_commands
|
|
.push(DrawCommand::image(start..mesh.vertices().len(), id));
|
|
start = mesh.vertices().len();
|
|
current_state = State::Plain;
|
|
}
|
|
};
|
|
}
|
|
|
|
while let Some(prim) = primitives.next() {
|
|
let Primitive {
|
|
kind,
|
|
scizzor,
|
|
rect,
|
|
id: widget_id,
|
|
} = prim;
|
|
// Check for a change in the scissor.
|
|
let new_scissor = {
|
|
let (l, b, w, h) = scizzor.l_b_w_h();
|
|
let scale_factor = self.scale.scale_factor_physical();
|
|
// Calculate minimum x and y coordinates while
|
|
// flipping y axis (from +up to +down) and
|
|
// moving origin to top-left corner (from middle).
|
|
let min_x = ui.win_w / 2.0 + l;
|
|
let min_y = ui.win_h / 2.0 - b - h;
|
|
let intersection = Aabr {
|
|
min: Vec2 {
|
|
x: (min_x * scale_factor) as u16,
|
|
y: (min_y * scale_factor) as u16,
|
|
},
|
|
max: Vec2 {
|
|
x: ((min_x + w) * scale_factor) as u16,
|
|
y: ((min_y + h) * scale_factor) as u16,
|
|
},
|
|
}
|
|
.intersection(window_scissor);
|
|
|
|
if intersection.is_valid() {
|
|
intersection
|
|
} else {
|
|
Aabr::new_empty(Vec2::zero())
|
|
}
|
|
};
|
|
if new_scissor != current_scissor {
|
|
// Finish the current command.
|
|
self.draw_commands.push(match current_state {
|
|
State::Plain => DrawCommand::plain(start..mesh.vertices().len()),
|
|
State::Image(id) => DrawCommand::image(start..mesh.vertices().len(), id),
|
|
});
|
|
start = mesh.vertices().len();
|
|
|
|
// Update the scissor and produce a command.
|
|
current_scissor = new_scissor;
|
|
self.draw_commands.push(DrawCommand::Scissor(new_scissor));
|
|
}
|
|
|
|
match placement {
|
|
// No primitives left to place in the world at the current position, go back to
|
|
// drawing the interface
|
|
Placement::InWorld(0, _) => {
|
|
placement = Placement::Interface;
|
|
// Finish current state
|
|
self.draw_commands.push(match current_state {
|
|
State::Plain => DrawCommand::plain(start..mesh.vertices().len()),
|
|
State::Image(id) => DrawCommand::image(start..mesh.vertices().len(), id),
|
|
});
|
|
start = mesh.vertices().len();
|
|
// Push new position command
|
|
self.draw_commands.push(DrawCommand::WorldPos(None));
|
|
},
|
|
// Primitives still left to draw ingame
|
|
Placement::InWorld(num_prims, visible) => match kind {
|
|
// Other types aren't drawn & shouldn't decrement the number of primitives left
|
|
// to draw ingame
|
|
PrimitiveKind::Other(_) => {},
|
|
// Decrement the number of primitives left
|
|
_ => {
|
|
placement = Placement::InWorld(num_prims - 1, visible);
|
|
// Behind the camera
|
|
if !visible {
|
|
continue;
|
|
}
|
|
},
|
|
},
|
|
Placement::Interface => {},
|
|
}
|
|
|
|
match kind {
|
|
PrimitiveKind::Image {
|
|
image_id,
|
|
color,
|
|
source_rect,
|
|
} => {
|
|
let (graphic_id, rotation) = self
|
|
.image_map
|
|
.get(&image_id)
|
|
.expect("Image does not exist in image map");
|
|
let gl_aabr = gl_aabr(rect);
|
|
let (source_aabr, gl_size) = {
|
|
// Transform the source rectangle into uv coordinate.
|
|
// TODO: Make sure this is right. Especially the conversions.
|
|
let ((uv_l, uv_r, uv_b, uv_t), gl_size) =
|
|
match graphic_cache.get_graphic(*graphic_id) {
|
|
Some(Graphic::Blank) | None => continue,
|
|
Some(Graphic::Image(image, ..)) => {
|
|
source_rect.and_then(|src_rect| {
|
|
let (image_w, image_h) = image.dimensions();
|
|
let (source_w, source_h) = src_rect.w_h();
|
|
let gl_size = gl_aabr.size();
|
|
if image_w == 0
|
|
|| image_h == 0
|
|
|| source_w < 1.0
|
|
|| source_h < 1.0
|
|
|| gl_size.reduce_partial_min() < f32::EPSILON
|
|
{
|
|
None
|
|
} else {
|
|
// Multiply drawn image size by ratio of original image
|
|
// size to
|
|
// source rectangle size (since as the proportion of the
|
|
// image gets
|
|
// smaller, the drawn size should get bigger), up to the
|
|
// actual
|
|
// size of the original image.
|
|
let ratio_x = (image_w as f64 / source_w).min(
|
|
(image_w as f64 / (gl_size.w * half_res.x) as f64)
|
|
.max(1.0),
|
|
);
|
|
let ratio_y = (image_h as f64 / source_h).min(
|
|
(image_h as f64 / (gl_size.h * half_res.y) as f64)
|
|
.max(1.0),
|
|
);
|
|
let (l, r, b, t) = src_rect.l_r_b_t();
|
|
Some((
|
|
(
|
|
l / image_w as f64, /* * ratio_x*/
|
|
r / image_w as f64, /* * ratio_x*/
|
|
b / image_h as f64, /* * ratio_y*/
|
|
t / image_h as f64, /* * ratio_y*/
|
|
),
|
|
Extent2::new(
|
|
(gl_size.w as f64 * ratio_x) as f32,
|
|
(gl_size.h as f64 * ratio_y) as f32,
|
|
),
|
|
))
|
|
/* ((l / image_w as f64),
|
|
(r / image_w as f64),
|
|
(b / image_h as f64),
|
|
(t / image_h as f64)) */
|
|
}
|
|
})
|
|
},
|
|
// No easy way to interpret source_rect for voxels...
|
|
Some(Graphic::Voxel(..)) => None,
|
|
}
|
|
.unwrap_or_else(|| ((0.0, 1.0, 0.0, 1.0), gl_aabr.size()));
|
|
(
|
|
Aabr {
|
|
min: Vec2::new(uv_l, uv_b),
|
|
max: Vec2::new(uv_r, uv_t),
|
|
},
|
|
gl_size,
|
|
)
|
|
};
|
|
|
|
let resolution = Vec2::new(
|
|
(gl_size.w * half_res.x).round() as u16,
|
|
(gl_size.h * half_res.y).round() as u16,
|
|
);
|
|
|
|
// Don't do anything if resolution is zero
|
|
if resolution.map(|e| e == 0).reduce_or() {
|
|
continue;
|
|
// TODO: consider logging unneeded elements
|
|
}
|
|
|
|
let color =
|
|
srgba_to_linear(color.unwrap_or(conrod_core::color::WHITE).to_fsa().into());
|
|
|
|
// Cache graphic at particular resolution.
|
|
let (uv_aabr, tex_id) = match graphic_cache.cache_res(
|
|
renderer,
|
|
*graphic_id,
|
|
resolution,
|
|
source_aabr,
|
|
*rotation,
|
|
) {
|
|
// TODO: get dims from graphic_cache (or have it return floats directly)
|
|
Some((aabr, tex_id)) => {
|
|
let cache_dims = graphic_cache
|
|
.get_tex(tex_id)
|
|
.0
|
|
.get_dimensions()
|
|
.xy()
|
|
.map(|e| e as f32);
|
|
let min = Vec2::new(aabr.min.x as f32, aabr.max.y as f32) / cache_dims;
|
|
let max = Vec2::new(aabr.max.x as f32, aabr.min.y as f32) / cache_dims;
|
|
(Aabr { min, max }, tex_id)
|
|
},
|
|
None => continue,
|
|
};
|
|
|
|
match current_state {
|
|
// Switch to the image state if we are not in it already.
|
|
State::Plain => {
|
|
self.draw_commands
|
|
.push(DrawCommand::plain(start..mesh.vertices().len()));
|
|
start = mesh.vertices().len();
|
|
current_state = State::Image(tex_id);
|
|
},
|
|
// If the image is cached in a different texture switch to the new one
|
|
State::Image(id) if id != tex_id => {
|
|
self.draw_commands
|
|
.push(DrawCommand::image(start..mesh.vertices().len(), id));
|
|
start = mesh.vertices().len();
|
|
current_state = State::Image(tex_id);
|
|
},
|
|
State::Image(_) => {},
|
|
}
|
|
|
|
mesh.push_quad(create_ui_quad(gl_aabr, uv_aabr, color, match *rotation {
|
|
Rotation::None | Rotation::Cw90 | Rotation::Cw180 | Rotation::Cw270 => {
|
|
UiMode::Image
|
|
},
|
|
Rotation::SourceNorth => UiMode::ImageSourceNorth,
|
|
Rotation::TargetNorth => UiMode::ImageTargetNorth,
|
|
}));
|
|
},
|
|
PrimitiveKind::Text { .. } => {
|
|
switch_to_plain_state!();
|
|
|
|
// Mesh should already be cached.
|
|
mesh.push_mesh(
|
|
text_cache
|
|
.get(&widget_id)
|
|
.as_deref()
|
|
.unwrap_or(&Mesh::new()),
|
|
);
|
|
},
|
|
PrimitiveKind::Rectangle { color } => {
|
|
let color = srgba_to_linear(color.to_fsa().into());
|
|
// Don't draw a transparent rectangle.
|
|
if color[3] == 0.0 {
|
|
continue;
|
|
}
|
|
|
|
switch_to_plain_state!();
|
|
|
|
mesh.push_quad(create_ui_quad(
|
|
gl_aabr(rect),
|
|
Aabr {
|
|
min: Vec2::zero(),
|
|
max: Vec2::zero(),
|
|
},
|
|
color,
|
|
UiMode::Geometry,
|
|
));
|
|
},
|
|
PrimitiveKind::TrianglesSingleColor { color, triangles } => {
|
|
// Don't draw transparent triangle or switch state if there are actually no
|
|
// triangles.
|
|
let color = srgba_to_linear(Rgba::from(Into::<[f32; 4]>::into(color)));
|
|
if triangles.is_empty() || color[3] == 0.0 {
|
|
continue;
|
|
}
|
|
|
|
switch_to_plain_state!();
|
|
|
|
for tri in triangles {
|
|
let p1 = Vec2::new(vx(tri[0][0]), vy(tri[0][1]));
|
|
let p2 = Vec2::new(vx(tri[1][0]), vy(tri[1][1]));
|
|
let p3 = Vec2::new(vx(tri[2][0]), vy(tri[2][1]));
|
|
// If triangle is clockwise, reverse it.
|
|
let (v1, v2): (Vec3<f32>, Vec3<f32>) = ((p2 - p1).into(), (p3 - p1).into());
|
|
let triangle = if v1.cross(v2).z > 0.0 {
|
|
[p1.into_array(), p2.into_array(), p3.into_array()]
|
|
} else {
|
|
[p2.into_array(), p1.into_array(), p3.into_array()]
|
|
};
|
|
mesh.push_tri(create_ui_tri(
|
|
triangle,
|
|
[[0.0; 2]; 3],
|
|
color,
|
|
UiMode::Geometry,
|
|
));
|
|
}
|
|
},
|
|
PrimitiveKind::Other(container) => {
|
|
if container.type_id == std::any::TypeId::of::<widgets::ingame::State>() {
|
|
// Calculate the scale factor to pixels at this 3d point using the camera.
|
|
if let Some(view_projection_mat) = view_projection_mat {
|
|
// Retrieve world position
|
|
let parameters = container
|
|
.state_and_style::<widgets::ingame::State, widgets::ingame::Style>()
|
|
.unwrap()
|
|
.state
|
|
.parameters;
|
|
|
|
let pos_on_screen = (view_projection_mat
|
|
* Vec4::from_point(parameters.pos))
|
|
.homogenized();
|
|
let visible = if pos_on_screen.z > 0.0 && pos_on_screen.z < 1.0 {
|
|
let x = pos_on_screen.x;
|
|
let y = pos_on_screen.y;
|
|
let (w, h) = parameters.dims.into_tuple();
|
|
let (half_w, half_h) = (w / ui_win_w as f32, h / ui_win_h as f32);
|
|
(x - half_w < 1.0 && x + half_w > -1.0)
|
|
&& (y - half_h < 1.0 && y + half_h > -1.0)
|
|
} else {
|
|
false
|
|
};
|
|
// Don't process ingame elements outside the frustum
|
|
placement = if visible {
|
|
// Finish current state
|
|
self.draw_commands.push(match current_state {
|
|
State::Plain => {
|
|
DrawCommand::plain(start..mesh.vertices().len())
|
|
},
|
|
State::Image(id) => {
|
|
DrawCommand::image(start..mesh.vertices().len(), id)
|
|
},
|
|
});
|
|
start = mesh.vertices().len();
|
|
|
|
// Push new position command
|
|
let world_pos = Vec4::from_point(parameters.pos);
|
|
if self.ingame_locals.len() > ingame_local_index {
|
|
renderer.update_consts(
|
|
&mut self.ingame_locals[ingame_local_index],
|
|
&[world_pos.into()],
|
|
)
|
|
} else {
|
|
self.ingame_locals
|
|
.push(renderer.create_ui_bound_locals(&[world_pos.into()]));
|
|
}
|
|
self.draw_commands
|
|
.push(DrawCommand::WorldPos(Some(ingame_local_index)));
|
|
ingame_local_index += 1;
|
|
|
|
Placement::InWorld(parameters.num, true)
|
|
} else {
|
|
Placement::InWorld(parameters.num, false)
|
|
};
|
|
}
|
|
}
|
|
},
|
|
_ => {}, /* TODO: Add this.
|
|
*PrimitiveKind::TrianglesMultiColor {..} => {println!("primitive kind
|
|
* multicolor with id {:?}", id);} */
|
|
}
|
|
}
|
|
// Enter the final command.
|
|
self.draw_commands.push(match current_state {
|
|
State::Plain => DrawCommand::plain(start..mesh.vertices().len()),
|
|
State::Image(id) => DrawCommand::image(start..mesh.vertices().len(), id),
|
|
});
|
|
|
|
/* // Draw glyph cache (use for debugging).
|
|
self.draw_commands
|
|
.push(DrawCommand::Scissor(default_scissor(renderer)));
|
|
start = mesh.vertices().len();
|
|
mesh.push_quad(create_ui_quad(
|
|
Aabr {
|
|
min: (-1.0, -1.0).into(),
|
|
max: (1.0, 1.0).into(),
|
|
},
|
|
Aabr {
|
|
min: (0.0, 1.0).into(),
|
|
max: (1.0, 0.0).into(),
|
|
},
|
|
Rgba::new(1.0, 1.0, 1.0, 0.8),
|
|
UiMode::Text,
|
|
));
|
|
self.draw_commands
|
|
.push(DrawCommand::plain(start..mesh.vertices().len())); */
|
|
|
|
// Create a larger dynamic model if the mesh is larger than the current model
|
|
// size.
|
|
if self.model.len() < self.mesh.vertices().len() {
|
|
self.model = renderer.create_dynamic_model(self.mesh.vertices().len() * 4 / 3);
|
|
}
|
|
// Update model with new mesh.
|
|
renderer.update_model(&self.model, &self.mesh, 0);
|
|
}
|
|
|
|
pub fn render<'pass, 'data: 'pass>(&'data self, drawer: &mut UiDrawer<'_, 'pass>) {
|
|
span!(_guard, "render", "Ui::render");
|
|
let mut drawer = drawer.prepare(&self.interface_locals, &self.model, self.window_scissor);
|
|
for draw_command in self.draw_commands.iter() {
|
|
match draw_command {
|
|
DrawCommand::Scissor(new_scissor) => {
|
|
drawer.set_scissor(*new_scissor);
|
|
},
|
|
DrawCommand::WorldPos(index) => {
|
|
drawer.set_locals(
|
|
index.map_or(&self.interface_locals, |i| &self.ingame_locals[i]),
|
|
);
|
|
},
|
|
DrawCommand::Draw { kind, verts } => {
|
|
let tex = match kind {
|
|
DrawKind::Image(tex_id) => self.cache.graphic_cache().get_tex(*tex_id),
|
|
DrawKind::Plain => self.cache.glyph_cache_tex(),
|
|
};
|
|
drawer.draw(&tex.1, verts.clone()); // Note: trivial clone
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_scissor(physical_resolution: Vec2<u32>) -> Aabr<u16> {
|
|
let (screen_w, screen_h) = physical_resolution.into_tuple();
|
|
Aabr {
|
|
min: Vec2 { x: 0, y: 0 },
|
|
max: Vec2 {
|
|
x: screen_w as u16,
|
|
y: screen_h as u16,
|
|
},
|
|
}
|
|
}
|