added text box ui widget

This commit is contained in:
Dmitry
2019-10-24 22:23:51 +03:00
parent 072e42cc37
commit 372d237591
24 changed files with 942 additions and 149 deletions
+21 -17
View File
@@ -7,23 +7,27 @@
<list default="true" id="1932b2c4-d6ff-413c-9558-19959667326b" name="Default Changelist" comment="scene graph">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/renderer/deferred_light_renderer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/renderer/deferred_light_renderer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/renderer/gbuffer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/renderer/gbuffer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/renderer/particle_system_renderer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/renderer/particle_system_renderer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/renderer/shadow_map_renderer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/renderer/shadow_map_renderer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/renderer/sprite_renderer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/renderer/sprite_renderer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/resource/fbx/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/resource/fbx/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/resource/model.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/resource/model.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/animation.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/animation.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/camera.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/camera.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/graph.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/graph.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/light.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/light.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/mesh.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/mesh.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/node.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/node.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/particle_system.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/particle_system.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/pivot.rs" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/scene/sprite.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/scene/sprite.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/engine/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/engine/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/border.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/border.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/button.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/button.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/canvas.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/canvas.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/draw.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/draw.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/event.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/event.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/formatted_text.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/formatted_text.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/grid.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/grid.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/image.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/image.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/list_box.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/list_box.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/mod.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/node.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/node.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/scroll_bar.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/scroll_bar.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/scroll_content_presenter.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/scroll_content_presenter.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/scroll_viewer.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/scroll_viewer.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/stack_panel.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/stack_panel.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/text.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/text.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/widget.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/widget.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/gui/window.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/gui/window.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/resource/ttf.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/resource/ttf.rs" afterDir="false" />
</list>
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" />
Generated
+1 -1
View File
@@ -883,7 +883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rg3d"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"glutin 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rg3d"
version = "0.2.1"
version = "0.2.2"
authors = ["Dmitry Stepanov <[email protected]>"]
edition = "2018"
license = "MIT"
+1 -1
View File
@@ -159,7 +159,7 @@ impl Engine {
self.sound_context.lock().unwrap().update().unwrap();
self.user_interface.update(Vec2::new(client_size.width as f32, client_size.height as f32));
self.user_interface.update(Vec2::new(client_size.width as f32, client_size.height as f32), dt);
}
pub fn get_ui_mut(&mut self) -> &mut UserInterface {
+11 -1
View File
@@ -7,7 +7,10 @@ use rg3d_core::{
},
};
use crate::gui::{
draw::{CommandKind, DrawingContext},
draw::{
CommandKind,
DrawingContext
},
Thickness,
UserInterface,
Layout,
@@ -19,6 +22,7 @@ use crate::gui::{
AsWidget
},
draw::CommandTexture,
Update,
};
pub struct Border {
@@ -37,6 +41,12 @@ impl AsWidget for Border {
}
}
impl Update for Border {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
pub struct BorderBuilder {
widget_builder: WidgetBuilder,
stroke_thickness: Option<Thickness>,
+7
View File
@@ -15,6 +15,7 @@ use crate::gui::{
Layout,
Draw,
draw::DrawingContext,
Update,
};
use rg3d_core::{
color::Color,
@@ -57,6 +58,12 @@ impl Draw for Button {
}
}
impl Update for Button {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
pub enum ButtonContent {
Text(String),
Node(Handle<UINode>),
+7
View File
@@ -16,6 +16,7 @@ use crate::gui::{
UserInterface,
Draw,
draw::DrawingContext,
Update
};
@@ -48,6 +49,12 @@ impl Default for Canvas {
}
}
impl Update for Canvas {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for Canvas {
fn measure_override(&self, ui: &UserInterface, _available_size: Vec2) -> Vec2 {
let size_for_child = Vec2::new(
+8 -1
View File
@@ -313,6 +313,13 @@ impl DrawingContext {
}
pub fn draw_text(&mut self, position: Vec2, formatted_text: &FormattedText) {
let font = if let Some(font) = formatted_text.get_font() {
font
} else {
println!("Trying to draw text without font!");
return;
};
for element in formatted_text.get_glyphs() {
let bounds = element.get_bounds();
@@ -322,7 +329,7 @@ impl DrawingContext {
self.push_rect_filled(&final_bounds, Some(element.get_tex_coords()), element.get_color());
}
self.commit(CommandKind::Geometry, CommandTexture::Font(formatted_text.get_font()))
self.commit(CommandKind::Geometry, CommandTexture::Font(font))
}
pub fn commit_clip_rect(&mut self, clip_rect: &Rect<f32>) {
+3
View File
@@ -67,8 +67,11 @@ pub enum UIEventKind {
},
MaxValueChanged(f32),
MinValueChanged(f32),
SelectionChanged(Option<usize>),
/// Any kind of user-defined event.
User(Box<dyn Any>),
}
+125 -72
View File
@@ -1,7 +1,9 @@
use rg3d_core::{
color::Color,
math::vec2::Vec2,
math::Rect
math::{
vec2::Vec2,
Rect
}
};
use std::{
rc::Rc,
@@ -11,6 +13,7 @@ use crate::{
resource::ttf::Font,
gui::{HorizontalAlignment, VerticalAlignment}
};
use std::ops::Range;
#[derive(Debug)]
pub struct TextGlyph {
@@ -34,11 +37,19 @@ impl TextGlyph {
}
#[derive(Copy, Clone, Debug)]
struct TextLine {
begin: usize,
end: usize,
width: f32,
x_offset: f32,
pub struct TextLine {
/// Index of starting symbol in text array.
pub begin: usize,
/// Index of ending symbol in text array.
pub end: usize,
/// Total width of line.
pub width: f32,
/// Total height of line. Usually just ascender of a font.
pub height: f32,
/// Local horizontal position of line.
pub x_offset: f32,
/// Local vertical position of line.
pub y_offset: f32,
}
impl TextLine {
@@ -47,13 +58,19 @@ impl TextLine {
begin: 0,
end: 0,
width: 0.0,
height: 0.0,
x_offset: 0.0,
y_offset: 0.0,
}
}
pub fn len(&self) -> usize {
self.end - self.begin
}
}
pub struct FormattedText {
font: Rc<RefCell<Font>>,
font: Option<Rc<RefCell<Font>>>,
/// Text in UTF32 format.
text: Vec<u32>,
/// Temporary buffer used to split text on lines. We need it to reduce memory allocations
@@ -62,35 +79,87 @@ pub struct FormattedText {
lines: Vec<TextLine>,
/// Final glyphs for draw buffer.
glyphs: Vec<TextGlyph>,
vertical_alignment: VerticalAlignment,
horizontal_alignment: HorizontalAlignment,
color: Color,
size: Vec2,
}
impl FormattedText {
fn new(font: Rc<RefCell<Font>>) -> FormattedText {
FormattedText {
text: Vec::new(),
font,
glyphs: Vec::new(),
lines: Vec::new(),
}
}
pub fn get_glyphs(&self) -> &[TextGlyph] {
&self.glyphs
}
pub fn get_font(&self) -> Rc<RefCell<Font>> {
pub fn get_font(&self) -> Option<Rc<RefCell<Font>>> {
self.font.clone()
}
fn build(&mut self, text: &str, size: Vec2, color: Color, vertical_alignment: VerticalAlignment,
horizontal_alignment: HorizontalAlignment) {
pub fn set_font(&mut self, font: Rc<RefCell<Font>>) {
self.font = Some(font);
}
pub fn get_lines(&self) -> &[TextLine] {
&self.lines
}
pub fn set_vertical_alignment(&mut self, vertical_alignment: VerticalAlignment ) {
self.vertical_alignment = vertical_alignment
}
pub fn set_horizontal_alignment(&mut self, horizontal_alignment: HorizontalAlignment ) {
self.horizontal_alignment = horizontal_alignment;
}
pub fn set_color(&mut self, color: Color) {
self.color = color;
}
pub fn set_size(&mut self, size: Vec2) {
self.size = size;
}
pub fn get_raw_text(&self) -> &[u32] {
&self.text
}
pub fn get_range_width(&self, range: Range<usize>) -> f32 {
let mut width = 0.0;
if let Some(ref font) = self.font {
let font = font.borrow();
for index in range {
width += font.get_glyph_advance(self.text[index]);
}
}
width
}
pub fn set_text(&mut self, text: &str) {
// Convert text to UTF32.
self.text.clear();
for code in text.chars().map(|c| c as u32) {
self.text.push(code);
}
}
let font = self.font.borrow();
pub fn insert_char(&mut self, c: char, index: usize) {
let c = c as u32;
if index == self.text.len() {
self.text.push(c);
} else {
self.text.insert(index, c);
}
}
pub fn remove_at(&mut self, index: usize) {
self.text.remove(index);
}
pub fn build(&mut self) {
let font = if let Some(font) = &self.font {
font.borrow()
} else {
return;
};
// Split on lines.
let mut total_height = 0.0;
@@ -104,7 +173,7 @@ impl FormattedText {
};
let is_new_line = *code == u32::from(b'\n') || *code == u32::from(b'\r');
let new_width = current_line.width + advance;
if new_width > size.x || is_new_line {
if new_width > self.size.x || is_new_line {
self.lines.push(current_line);
current_line.begin = if is_new_line { i + 1 } else { i };
current_line.end = current_line.begin + 1;
@@ -124,10 +193,10 @@ impl FormattedText {
// Align lines according to desired alignment.
for line in self.lines.iter_mut() {
match horizontal_alignment {
match self.horizontal_alignment {
HorizontalAlignment::Left => line.x_offset = 0.0,
HorizontalAlignment::Center => line.x_offset = 0.5 * (size.x - line.width),
HorizontalAlignment::Right => line.x_offset = size.x - line.width,
HorizontalAlignment::Center => line.x_offset = 0.5 * (self.size.x - line.width),
HorizontalAlignment::Right => line.x_offset = self.size.x - line.width,
HorizontalAlignment::Stretch => line.x_offset = 0.0
}
}
@@ -135,15 +204,15 @@ impl FormattedText {
// Generate glyphs for each text line.
self.glyphs.clear();
let cursor_y_start = match vertical_alignment {
let cursor_y_start = match self.vertical_alignment {
VerticalAlignment::Top => 0.0,
VerticalAlignment::Center => (size.y - total_height) * 0.5,
VerticalAlignment::Bottom => size.y - total_height,
VerticalAlignment::Center => (self.size.y - total_height) * 0.5,
VerticalAlignment::Bottom => self.size.y - total_height,
VerticalAlignment::Stretch => 0.0
};
let mut cursor = Vec2::new(size.x, cursor_y_start);
for line in self.lines.iter() {
let mut cursor = Vec2::new(self.size.x, cursor_y_start);
for line in self.lines.iter_mut() {
cursor.x = line.x_offset;
for code_index in line.begin..line.end {
@@ -162,7 +231,7 @@ impl FormattedText {
let text_glyph = TextGlyph {
bounds: rect,
tex_coords: *glyph.get_tex_coords(),
color,
color: self.color,
};
self.glyphs.push(text_glyph);
}
@@ -179,33 +248,34 @@ impl FormattedText {
self.glyphs.push(TextGlyph {
bounds: rect,
tex_coords: [Vec2::ZERO; 4],
color,
color: self.color,
});
cursor.x += rect.w;
}
}
}
line.height = font.get_ascender();
line.y_offset = cursor.y;
cursor.y += font.get_ascender();
}
}
}
pub struct FormattedTextBuilder<'a> {
pub struct FormattedTextBuilder {
font: Option<Rc<RefCell<Font>>>,
color: Color,
size: Vec2,
text: Option<&'a str>,
formatted_text: FormattedText,
text: String,
vertical_alignment: VerticalAlignment,
horizontal_alignment: HorizontalAlignment,
}
impl<'a> FormattedTextBuilder<'a> {
impl FormattedTextBuilder {
/// Creates new formatted text builder with default parameters.
pub fn new(font: Rc<RefCell<Font>>) -> FormattedTextBuilder<'a> {
pub fn new() -> FormattedTextBuilder {
FormattedTextBuilder {
text: None,
formatted_text: FormattedText::new(font),
font: None,
text: "".to_owned(),
horizontal_alignment: HorizontalAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
color: Color::WHITE,
@@ -213,25 +283,9 @@ impl<'a> FormattedTextBuilder<'a> {
}
}
/// Creates new formatted text builder that will reuse existing
/// buffers from existing formatted text. This is very useful to
/// reduce memory allocations.
pub fn reuse(formatted_text: FormattedText) -> FormattedTextBuilder<'a> {
FormattedTextBuilder {
text: None,
formatted_text: FormattedText {
// Take buffers out and reuse them so no need to allocate new
// buffers every time when need to change a text.
text: formatted_text.text,
lines: formatted_text.lines,
glyphs: formatted_text.glyphs,
font: formatted_text.font,
},
horizontal_alignment: HorizontalAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
color: Color::WHITE,
size: Vec2::new(128.0, 128.0),
}
pub fn with_font(mut self, font: Rc<RefCell<Font>>) -> Self {
self.font = Some(font);
self
}
pub fn with_vertical_alignment(mut self, vertical_alignment: VerticalAlignment) -> Self {
@@ -244,8 +298,8 @@ impl<'a> FormattedTextBuilder<'a> {
self
}
pub fn with_text(mut self, text: &'a str) -> Self {
self.text = Some(text);
pub fn with_text(mut self, text: String) -> Self {
self.text = text;
self
}
@@ -259,17 +313,16 @@ impl<'a> FormattedTextBuilder<'a> {
self
}
pub fn build(mut self) -> FormattedText {
if let Some(text) = self.text {
self.formatted_text.build(
text,
self.size,
self.color,
self.vertical_alignment,
self.horizontal_alignment,
);
pub fn build(self) -> FormattedText {
FormattedText {
font: self.font,
text: self.text.chars().map(|c| c as u32).collect(),
lines: Vec::new(),
glyphs: Vec::new(),
vertical_alignment: self.vertical_alignment,
horizontal_alignment: self.horizontal_alignment,
color: self.color,
size: Vec2::new(120.0, 120.0)
}
self.formatted_text
}
}
+7
View File
@@ -14,6 +14,7 @@ use crate::gui::{
UserInterface,
draw::DrawingContext,
widget::{WidgetBuilder, Widget, AsWidget},
Update
};
#[derive(PartialEq)]
@@ -136,6 +137,12 @@ impl Draw for Grid {
}
}
impl Update for Grid {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for Grid {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
// In case of no rows or columns, grid acts like default panel.
+8 -1
View File
@@ -6,7 +6,8 @@ use crate::{
draw::{DrawingContext, CommandKind, CommandTexture},
widget::{Widget, AsWidget},
Layout,
UserInterface
UserInterface,
Update
}
};
use rg3d_core::math::vec2::Vec2;
@@ -42,4 +43,10 @@ impl Layout for Image {
fn arrange_override(&self, ui: &UserInterface, final_size: Vec2) -> Vec2 {
self.widget.arrange_override(ui, final_size)
}
}
impl Update for Image {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
+80 -4
View File
@@ -14,15 +14,39 @@ use crate::gui::{
Layout,
draw::DrawingContext,
stack_panel::StackPanelBuilder,
event::{
UIEventKind,
UIEvent,
},
Update
};
pub struct ListBox {
widget: Widget,
panel: Handle<UINode>,
selected_index: Option<usize>,
items: Vec<Handle<UINode>>,
}
impl ListBox {}
impl ListBox {
pub fn set_selected(&mut self, new_index: Option<usize>) {
let old_value = self.selected_index;
self.selected_index = new_index;
if old_value.is_none() && new_index.is_some() ||
old_value.is_some() && new_index.is_none() ||
old_value.unwrap() != new_index.unwrap() {
self.widget.events
.borrow_mut()
.push_back(UIEvent::new(UIEventKind::SelectionChanged(self.selected_index)))
}
}
pub fn get_selected(&self) -> Option<usize> {
self.selected_index
}
}
impl AsWidget for ListBox {
fn widget(&self) -> &Widget {
@@ -40,6 +64,12 @@ impl Draw for ListBox {
}
}
impl Update for ListBox {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for ListBox {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
self.widget.measure_override(ui, available_size)
@@ -52,7 +82,7 @@ impl Layout for ListBox {
pub struct ListBoxBuilder {
widget_builder: WidgetBuilder,
items: Vec<Handle<UINode>>
items: Vec<Handle<UINode>>,
}
impl ListBoxBuilder {
@@ -69,8 +99,54 @@ impl ListBoxBuilder {
}
pub fn build(self, ui: &mut UserInterface) -> Handle<UINode> {
// Wrap each item into container which will have selection behaviour
let items: Vec<Handle<UINode>> = self.items.iter().enumerate().map(|(index, item)| {
BorderBuilder::new(WidgetBuilder::new()
.with_child(*item)
.with_event_handler(Box::new(move |ui, handle, evt| {
let list_box = ui.find_by_criteria_up(handle, |node| node.is_list_box());
if evt.source == handle || ui.is_node_child_of(evt.source, handle) {
let border = ui.get_node_mut(handle).as_border_mut();
match evt.kind {
UIEventKind::MouseLeave => {
border.widget_mut().set_color(Color::opaque(100, 100, 100));
}
UIEventKind::MouseEnter => {
border.widget_mut().set_color(Color::opaque(130, 130, 130));
}
UIEventKind::MouseDown { .. } => {
// Explicitly set selection on parent list box. This will send
// SelectionChanged event and all items will react.
let list_box = ui.get_node_mut(list_box).as_list_box_mut();
list_box.set_selected(Some(index))
}
_ => ()
}
} else if evt.source == list_box {
let border = ui.get_node_mut(handle).as_border_mut();
if let UIEventKind::SelectionChanged(new_value) = evt.kind {
// We know now that selection has changed in parent list box,
// check at which index and keep visual state according to it.
if let Some(new_value) = new_value {
if new_value == index {
border.set_stroke_color(Color::opaque(0, 0, 0));
border.set_stroke_thickness(Thickness::uniform(2.0));
return;
}
}
border.set_stroke_color(Color::opaque(80, 80, 80));
border.set_stroke_thickness(Thickness::uniform(1.0));
}
}
}))
.with_color(Color::opaque(80, 80, 80)))
.with_stroke_color(Color::opaque(60, 60, 60))
.with_stroke_thickness(Thickness::uniform(1.0))
.build(ui)
}).collect();
let panel = StackPanelBuilder::new(WidgetBuilder::new()
.with_children(self.items))
.with_children(&items))
.build(ui);
let scroll_viewer = ScrollViewerBuilder::new(WidgetBuilder::new()
@@ -80,7 +156,6 @@ impl ListBoxBuilder {
let list_box = UINode::ListBox(ListBox {
widget: self.widget_builder
.with_event_handler(Box::new(|ui, handle, evt| {}))
.with_child(BorderBuilder::new(WidgetBuilder::new()
.with_color(Color::opaque(100, 100, 100))
.with_child(scroll_viewer))
@@ -88,6 +163,7 @@ impl ListBoxBuilder {
.build(),
selected_index: None,
panel,
items,
});
ui.add_node(list_box)
+78 -7
View File
@@ -15,6 +15,7 @@ pub mod formatted_text;
pub mod widget;
pub mod list_box;
pub mod stack_panel;
pub mod text_box;
use std::{
collections::VecDeque,
@@ -27,11 +28,16 @@ use crate::{
widget::AsWidget,
draw::{DrawingContext, CommandKind, CommandTexture},
canvas::Canvas,
event::{UIEvent, UIEventKind},
event::{
UIEvent,
UIEventKind,
UIEventHandler
}
},
resource::{ttf::Font},
utils::UnsafeCollectionView,
ElementState, WindowEvent,
ElementState,
WindowEvent,
MouseScrollDelta,
};
use rg3d_core::{
@@ -39,7 +45,6 @@ use rg3d_core::{
pool::{Pool, Handle},
math::{vec2::Vec2, Rect},
};
use crate::gui::event::UIEventHandler;
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum HorizontalAlignment {
@@ -103,6 +108,10 @@ trait Draw {
fn draw(&mut self, drawing_context: &mut DrawingContext);
}
trait Update {
fn update(&mut self, dt: f32);
}
pub struct UserInterface {
nodes: Pool<UINode>,
drawing_context: DrawingContext,
@@ -113,9 +122,10 @@ pub struct UserInterface {
picked_node: Handle<UINode>,
prev_picked_node: Handle<UINode>,
captured_node: Handle<UINode>,
keyboard_focus_node: Handle<UINode>,
mouse_position: Vec2,
events: VecDeque<UIEvent>,
event_handlers: Vec<Option<Box<UIEventHandler>>>
event_handlers: Vec<Option<Box<UIEventHandler>>>,
}
#[inline]
@@ -152,7 +162,8 @@ impl UserInterface {
drawing_context: DrawingContext::new(),
picked_node: Handle::NONE,
prev_picked_node: Handle::NONE,
event_handlers: Vec::new()
event_handlers: Vec::new(),
keyboard_focus_node: Handle::NONE,
};
ui.root_canvas = ui.add_node(UINode::Canvas(Canvas::new()));
ui
@@ -355,7 +366,7 @@ impl UserInterface {
size.y = widget.height.get();
}
size = node.arrange_override( self, size);
size = node.arrange_override(self, size);
if size.x > final_rect.w {
size.x = final_rect.w;
@@ -407,10 +418,13 @@ impl UserInterface {
}
}
pub fn update(&mut self, screen_size: Vec2) {
pub fn update(&mut self, screen_size: Vec2, dt: f32) {
self.measure(self.root_canvas, screen_size);
self.arrange(self.root_canvas, &Rect::new(0.0, 0.0, screen_size.x, screen_size.y));
self.update_transform(self.root_canvas);
for node in self.nodes.iter_mut() {
node.update(dt)
}
}
fn draw_node(&mut self, node_handle: Handle<UINode>, nesting: u8) {
@@ -582,6 +596,8 @@ impl UserInterface {
self.find_by_criteria_up(node.widget().parent, func)
}
/// Checks if specified node is a child of some other node on `root_handle`. This method
/// is useful to understand if some event came from some node down by tree.
pub fn is_node_child_of(&self, node_handle: Handle<UINode>, root_handle: Handle<UINode>) -> bool {
for child_handle in self.nodes.borrow(root_handle).widget().children.iter() {
if *child_handle == node_handle {
@@ -596,6 +612,16 @@ impl UserInterface {
false
}
/// Checks if specified node is a direct child of some other node on `root_handle`.
pub fn is_node_direct_child_of(&self, node_handle: Handle<UINode>, root_handle: Handle<UINode>) -> bool {
for child_handle in self.nodes.borrow(root_handle).widget().children.iter() {
if *child_handle == node_handle {
return true;
}
}
false
}
/// Searches a node by name up on tree starting from given root node.
pub fn find_by_name_up(&self, node_handle: Handle<UINode>, name: &str) -> Handle<UINode> {
self.find_by_criteria_up(node_handle, |node| node.widget().name == name)
@@ -692,6 +718,8 @@ impl UserInterface {
ElementState::Pressed => {
self.picked_node = self.hit_test(self.mouse_position);
self.keyboard_focus_node = self.picked_node;
if !self.picked_node.is_none() {
self.events.push_back(UIEvent {
handled: false,
@@ -793,6 +821,49 @@ impl UserInterface {
}
}
}
WindowEvent::KeyboardInput { input, .. } => {
if self.keyboard_focus_node.is_some() {
if let Some(keycode) = input.virtual_keycode {
let event = UIEvent {
handled: false,
kind: match input.state {
ElementState::Pressed => {
UIEventKind::KeyDown {
code: keycode,
}
}
ElementState::Released => {
UIEventKind::KeyUp {
code: keycode,
}
}
},
target: Handle::NONE,
source: self.keyboard_focus_node,
};
self.events.push_back(event);
event_processed = true;
}
}
},
WindowEvent::ReceivedCharacter(unicode) => {
if self.keyboard_focus_node.is_some() {
let event = UIEvent {
handled: false,
kind: UIEventKind::Text {
symbol: *unicode
},
target: Handle::NONE,
source: self.keyboard_focus_node
};
self.events.push_back(event);
event_processed = true;
}
}
_ => ()
}
+13 -20
View File
@@ -1,23 +1,5 @@
use rg3d_core::math::vec2::Vec2;
use crate::gui::{
draw::DrawingContext,
button::Button,
text::Text,
border::Border,
scroll_bar::ScrollBar,
scroll_viewer::ScrollViewer,
image::Image,
grid::Grid,
scroll_content_presenter::ScrollContentPresenter,
window::Window,
Draw,
Layout,
UserInterface,
canvas::Canvas,
widget::{Widget, AsWidget},
};
use crate::gui::list_box::ListBox;
use crate::gui::stack_panel::StackPanel;
use crate::gui::{draw::DrawingContext, button::Button, text::Text, border::Border, scroll_bar::ScrollBar, scroll_viewer::ScrollViewer, image::Image, grid::Grid, scroll_content_presenter::ScrollContentPresenter, window::Window, Draw, Layout, UserInterface, canvas::Canvas, widget::{Widget, AsWidget}, list_box::ListBox, stack_panel::StackPanel, text_box::TextBox, Update};
/// UI node is a building block for all UI widgets. For example button could be a node with
/// this structure
@@ -50,7 +32,8 @@ pub enum UINode {
ScrollContentPresenter(ScrollContentPresenter),
Window(Window),
ListBox(ListBox),
StackPanel(StackPanel)
StackPanel(StackPanel),
TextBox(TextBox),
}
macro_rules! dispatch {
@@ -69,6 +52,7 @@ macro_rules! dispatch {
UINode::Window(v) => v.$func($($args),*),
UINode::ListBox(v) => v.$func($($args),*),
UINode::StackPanel(v) => v.$func($($args),*),
UINode::TextBox(v) => v.$func($($args),*),
}
};
}
@@ -79,6 +63,12 @@ impl Draw for UINode {
}
}
impl Update for UINode {
fn update(&mut self, dt: f32) {
dispatch!(self, update, dt)
}
}
impl Layout for UINode {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
dispatch!(self, measure_override, ui, available_size)
@@ -135,4 +125,7 @@ impl UINode {
define_is_as!(is_scroll_content_presenter, as_scroll_content_presenter,
as_scroll_content_presenter_mut, ScrollContentPresenter, ScrollContentPresenter);
define_is_as!(is_window, as_window, as_window_mut, Window, Window);
define_is_as!(is_list_box, as_list_box, as_list_box_mut, ListBox, ListBox);
define_is_as!(is_stack_panel, as_stack_panel, as_stack_panel_mut, StackPanel, StackPanel);
define_is_as!(is_text_box, as_text_box, as_text_box_mut, TextBox, TextBox);
}
+19
View File
@@ -13,6 +13,7 @@ use crate::gui::{
widget::{Widget, WidgetBuilder, AsWidget},
Draw,
draw::DrawingContext,
Update
};
use rg3d_core::{
color::Color, math,
@@ -55,6 +56,12 @@ impl Draw for ScrollBar {
}
}
impl Update for ScrollBar {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl ScrollBar {
pub const PART_CANVAS: &'static str = "PART_Canvas";
@@ -106,6 +113,18 @@ impl ScrollBar {
pub fn get_min_value(&self) -> f32 {
self.min
}
pub fn scroll(&mut self, amount: f32) {
let old_value = self.value;
self.value += amount;
self.widget.events.borrow_mut().push_back(
UIEvent::new(UIEventKind::NumericValueChanged {
old_value,
new_value: self.value,
}));
}
}
impl Layout for ScrollBar {
+7
View File
@@ -13,6 +13,7 @@ use crate::gui::{
AsWidget,
},
UserInterface, Layout, draw::DrawingContext,
Update
};
/// Allows user to scroll content
@@ -39,6 +40,12 @@ impl Draw for ScrollContentPresenter {
}
}
impl Update for ScrollContentPresenter {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for ScrollContentPresenter {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
let size_for_child = Vec2::new(
+29 -8
View File
@@ -1,18 +1,23 @@
use crate::gui::{
UserInterface, maxf,
UserInterface,
maxf,
node::UINode,
scroll_content_presenter::ScrollContentPresenterBuilder,
scroll_bar::{ScrollBarBuilder, Orientation},
grid::{Row, GridBuilder, Column},
event::UIEventKind, Layout, widget::{Widget, WidgetBuilder, AsWidget},
Draw, draw::DrawingContext,
event::UIEventKind,
Layout,
widget::{Widget, WidgetBuilder, AsWidget},
Draw,
draw::DrawingContext,
Visibility,
event::UIEvent,
Update
};
use rg3d_core::{
pool::Handle,
math::vec2::Vec2,
};
use crate::gui::event::UIEvent;
pub struct ScrollViewer {
widget: Widget,
@@ -55,6 +60,12 @@ impl Layout for ScrollViewer {
}
}
impl Update for ScrollViewer {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Draw for ScrollViewer {
fn draw(&mut self, drawing_context: &mut DrawingContext) {
self.widget.draw(drawing_context)
@@ -141,16 +152,26 @@ impl ScrollViewerBuilder {
.add_column(Column::stretch())
.add_column(Column::auto())
.build(ui))
.with_event_handler(Box::new(move |ui, _handle, event| {
if let UIEventKind::NumericValueChanged { new_value, .. } = event.kind {
let content_presenter = ui.get_node_mut(content_presenter);
if let UINode::ScrollContentPresenter(content_presenter) = content_presenter {
.with_event_handler(Box::new(move |ui, handle, event| {
match event.kind {
UIEventKind::NumericValueChanged { new_value, .. } => {
let content_presenter = ui.get_node_mut(content_presenter).as_scroll_content_presenter_mut();
if event.source == h_scroll_bar {
content_presenter.set_horizontal_scroll(new_value);
} else if event.source == v_scroll_bar {
content_presenter.set_vertical_scroll(new_value);
}
}
UIEventKind::MouseWheel { amount, .. } => {
if !event.handled {
if event.source == handle || ui.is_node_child_of(event.source, handle) {
let v_scroll_bar = ui.get_node_mut(v_scroll_bar).as_scroll_bar_mut();
v_scroll_bar.scroll(-amount * 10.0);
event.handled = true;
}
}
}
_ => {}
}
}))
.build(),
+7
View File
@@ -7,6 +7,7 @@ use crate::gui::{
draw::DrawingContext,
node::UINode,
scroll_bar::Orientation,
Update
};
use rg3d_core::{
math::{
@@ -27,6 +28,12 @@ impl Draw for StackPanel {
}
}
impl Update for StackPanel {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for StackPanel {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
let mut child_constraint = Vec2::new(std::f32::INFINITY, std::f32::INFINITY);
+16 -12
View File
@@ -15,7 +15,7 @@ use crate::{
UserInterface,
formatted_text::{FormattedText, FormattedTextBuilder},
widget::{Widget, WidgetBuilder, AsWidget},
Layout
Layout, Update
},
resource::ttf::Font,
};
@@ -27,7 +27,7 @@ pub struct Text {
font: Rc<RefCell<Font>>,
vertical_alignment: VerticalAlignment,
horizontal_alignment: HorizontalAlignment,
formatted_text: Option<FormattedText>,
formatted_text: FormattedText,
}
impl AsWidget for Text {
@@ -40,6 +40,12 @@ impl AsWidget for Text {
}
}
impl Update for Text {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for Text {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
self.widget.measure_override(ui, available_size)
@@ -54,17 +60,15 @@ impl Draw for Text {
fn draw(&mut self, drawing_context: &mut DrawingContext) {
let bounds = self.widget.get_screen_bounds();
if self.need_update {
let formatted_text = FormattedTextBuilder::reuse(self.formatted_text.take().unwrap())
.with_size(Vec2::new(bounds.w, bounds.h))
.with_text(self.text.as_str())
.with_color(self.widget.color)
.with_horizontal_alignment(self.horizontal_alignment)
.with_vertical_alignment(self.vertical_alignment)
.build();
self.formatted_text = Some(formatted_text);
self.formatted_text.set_size(Vec2::new(bounds.w, bounds.h));
self.formatted_text.set_text(self.text.as_str());
self.formatted_text.set_color(self.widget.color);
self.formatted_text.set_horizontal_alignment(self.horizontal_alignment);
self.formatted_text.set_vertical_alignment(self.vertical_alignment);
self.formatted_text.build();
self.need_update = true; // TODO
}
drawing_context.draw_text(Vec2::new(bounds.x, bounds.y), self.formatted_text.as_ref().unwrap());
drawing_context.draw_text(Vec2::new(bounds.x, bounds.y), &self.formatted_text);
}
}
@@ -149,7 +153,7 @@ impl TextBuilder {
need_update: true,
vertical_alignment: self.vertical_text_alignment.unwrap_or(VerticalAlignment::Top),
horizontal_alignment: self.horizontal_text_alignment.unwrap_or(HorizontalAlignment::Left),
formatted_text: Some(FormattedTextBuilder::new(font.clone()).build()),
formatted_text: FormattedTextBuilder::new().with_font(font.clone()).build(),
font
}))
}
+468
View File
@@ -0,0 +1,468 @@
use rg3d_core::{
math::{
vec2::Vec2,
Rect,
},
pool::Handle,
color::Color,
};
use crate::{
gui::{
event::UIEventKind,
widget::{
WidgetBuilder,
AsWidget,
Widget,
},
Draw,
Layout,
UserInterface,
draw::{
DrawingContext,
CommandKind,
CommandTexture,
},
formatted_text::{
FormattedText,
FormattedTextBuilder,
},
node::UINode,
Update,
},
VirtualKeyCode, resource::ttf::Font, MouseButton,
};
use std::{rc::Rc, cell::RefCell, cmp};
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum HorizontalDirection {
Left,
Right,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum VerticalDirection {
Down,
Up,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Position {
line: usize,
offset: usize,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct SelectionRange {
begin: Position,
end: Position,
}
pub struct TextBox {
widget: Widget,
caret_line: usize,
caret_offset: usize,
caret_visible: bool,
blink_timer: f32,
blink_interval: f32,
formatted_text: FormattedText,
selection_range: Option<SelectionRange>,
selecting: bool,
}
impl TextBox {
fn reset_blink(&mut self) {
self.caret_visible = true;
self.blink_timer = 0.0;
}
pub fn move_caret_x(&mut self, mut offset: usize, direction: HorizontalDirection) {
self.selection_range = None;
self.reset_blink();
let lines = self.formatted_text.get_lines();
if lines.is_empty() {
self.caret_offset = 0;
self.caret_line = 0;
return;
}
while offset > 0 {
match direction {
HorizontalDirection::Left => {
if self.caret_offset > 0 {
self.caret_offset -= 1
} else {
if self.caret_line > 0 {
self.caret_line -= 1;
self.caret_offset = lines[self.caret_line].len();
} else {
self.caret_offset = 0;
break;
}
}
}
HorizontalDirection::Right => {
let line = lines.get(self.caret_line).unwrap();
if self.caret_offset < line.len() {
self.caret_offset += 1;
} else {
if self.caret_line < lines.len() - 1 {
self.caret_line += 1;
self.caret_offset = 0;
} else {
self.caret_offset = line.len();
break;
}
}
}
}
offset -= 1;
}
}
pub fn move_caret_y(&mut self, offset: usize, direction: VerticalDirection) {
let lines = self.formatted_text.get_lines();
if lines.is_empty() {
return;
}
let line_count = lines.len();
match direction {
VerticalDirection::Down => {
if self.caret_line + offset >= line_count {
self.caret_line = line_count - 1;
} else {
self.caret_line += offset;
}
}
VerticalDirection::Up => {
if self.caret_line > offset {
self.caret_line -= offset;
} else {
self.caret_line = 0;
}
}
}
}
pub fn get_absolute_position(&self) -> Option<usize> {
if let Some(line) = self.formatted_text.get_lines().get(self.caret_line) {
Some(line.begin + cmp::min(self.caret_offset, line.len()))
} else {
None
}
}
/// Inserts given character at current caret position.
pub fn insert_char(&mut self, c: char) {
if !c.is_control() {
let position = self.get_absolute_position().unwrap_or(0);
self.formatted_text.insert_char(c, position);
self.formatted_text.build();
self.move_caret_x(1, HorizontalDirection::Right);
}
}
pub fn get_text_len(&self) -> usize {
self.formatted_text.get_raw_text().len()
}
pub fn remove_char(&mut self, direction: HorizontalDirection) {
if let Some(position) = self.get_absolute_position() {
let text_len = self.get_text_len();
if text_len != 0 {
let position = match direction {
HorizontalDirection::Left => {
if position == 0 {
return;
}
position - 1
}
HorizontalDirection::Right => {
if position >= text_len {
return;
}
position
}
};
self.formatted_text.remove_at(position);
self.formatted_text.build();
if direction == HorizontalDirection::Left {
self.move_caret_x(1, direction);
}
}
}
}
pub fn screen_pos_to_text_pos(&self, screen_pos: Vec2) -> Option<Position> {
let mut caret_pos = self.widget.screen_position;
if let Some(font) = self.formatted_text.get_font() {
let font = font.borrow();
for (line_index, line) in self.formatted_text.get_lines().iter().enumerate() {
let line_bounds =
Rect::new(caret_pos.x + line.x_offset, caret_pos.y, line.width, font.get_ascender());
if line_bounds.contains(screen_pos.x, screen_pos.y) {
let mut x = line_bounds.x;
// Check each character in line.
for (offset, index) in (line.begin..line.end).enumerate() {
let symbol = self.formatted_text.get_raw_text()[index];
let (width, height, advance) = if let Some(glyph) = font.get_glyph(symbol) {
(glyph.get_bitmap_width(), glyph.get_bitmap_height(), glyph.get_advance())
} else {
// Stub
let h = font.get_height();
(h, h, h)
};
let char_bounds = Rect::new(x, line_bounds.y, width, height);
if char_bounds.contains(screen_pos.x, screen_pos.y) {
return Some(Position { line: line_index, offset });
}
x += advance;
}
}
caret_pos.y += line_bounds.h;
}
}
None
}
}
impl AsWidget for TextBox {
fn widget(&self) -> &Widget {
&self.widget
}
fn widget_mut(&mut self) -> &mut Widget {
&mut self.widget
}
}
impl Draw for TextBox {
fn draw(&mut self, drawing_context: &mut DrawingContext) {
self.widget.draw(drawing_context);
let bounds = self.widget.get_screen_bounds();
drawing_context.push_rect_filled(&bounds, None, Color::opaque(80, 80, 80));
drawing_context.commit(CommandKind::Geometry, CommandTexture::None);
self.formatted_text.set_size(Vec2::new(bounds.w, bounds.h));
self.formatted_text.set_color(self.widget.color);
self.formatted_text.build();
if let Some(ref selection_range) = self.selection_range {
let text = &self.formatted_text;
let lines = text.get_lines();
if selection_range.begin.line == selection_range.end.line {
let line = lines[selection_range.begin.line];
let begin = selection_range.begin.offset;
let end = selection_range.end.offset;
// Begin line
let offset = text.get_range_width(line.begin..(line.begin + begin));
let width = text.get_range_width((line.begin + begin)..(line.begin + end));
let bounds = Rect::new(bounds.x + line.x_offset + offset,
bounds.y + line.y_offset,
width,
line.height);
drawing_context.push_rect_filled(&bounds, None, Color::opaque(65, 65, 90));
} else {
for (i, line) in text.get_lines().iter().enumerate() {
if i >= selection_range.begin.line && i <= selection_range.end.line {
let bounds = if i == selection_range.begin.line {
// Begin line
let offset = text.get_range_width(line.begin..(line.begin + selection_range.begin.offset));
let width = text.get_range_width((line.begin + selection_range.begin.offset)..line.end);
Rect::new(bounds.x + line.x_offset + offset,
bounds.y + line.y_offset,
width,
line.height)
} else if i == selection_range.end.line {
// End line
let width = text.get_range_width(line.begin..(line.begin + selection_range.end.offset));
Rect::new(bounds.x + line.x_offset,
bounds.y + line.y_offset,
width,
line.height)
} else {
// Everything between
Rect::new(bounds.x + line.x_offset,
bounds.y + line.y_offset,
line.width,
line.height)
};
drawing_context.push_rect_filled(&bounds, None, Color::opaque(90, 90, 120));
}
}
}
}
drawing_context.commit(CommandKind::Geometry, CommandTexture::None);
let screen_position = Vec2::new(bounds.x, bounds.y);
drawing_context.draw_text(screen_position, &self.formatted_text);
if self.caret_visible {
if let Some(font) = self.formatted_text.get_font() {
let font = font.borrow();
if let Some(line) = self.formatted_text.get_lines().get(self.caret_line) {
let text = self.formatted_text.get_raw_text();
let mut caret_pos = Vec2::new(
screen_position.x,
screen_position.y + self.caret_line as f32 * font.get_ascender(),
);
for (offset, char_index) in (line.begin..line.end).enumerate() {
if offset >= self.caret_offset {
break;
}
if let Some(glyph) = font.get_glyph(text[char_index]) {
caret_pos.x += glyph.get_advance();
} else {
caret_pos.x += font.get_height();
}
}
let caret_bounds = Rect::new(caret_pos.x, caret_pos.y, 2.0, font.get_height());
drawing_context.push_rect_filled(&caret_bounds, None, Color::WHITE);
drawing_context.commit(CommandKind::Geometry, CommandTexture::None);
}
}
}
}
}
impl Layout for TextBox {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
self.widget.measure_override(ui, available_size)
}
fn arrange_override(&self, ui: &UserInterface, final_size: Vec2) -> Vec2 {
self.widget.arrange_override(ui, final_size)
}
}
impl Update for TextBox {
fn update(&mut self, dt: f32) {
self.widget.update(dt);
self.blink_timer += dt;
if self.blink_timer >= self.blink_interval {
self.blink_timer = 0.0;
self.caret_visible = !self.caret_visible;
}
}
}
pub struct TextBoxBuilder {
widget_builder: WidgetBuilder,
font: Option<Rc<RefCell<Font>>>,
text: String,
}
impl TextBoxBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
font: None,
text: "".to_owned(),
}
}
pub fn with_font(mut self, font: Rc<RefCell<Font>>) -> Self {
self.font = Some(font);
self
}
pub fn with_text(mut self, text: String) -> Self {
self.text = text;
self
}
pub fn build(self, ui: &mut UserInterface) -> Handle<UINode> {
let text_box = UINode::TextBox(TextBox {
widget: self.widget_builder
.with_event_handler(Box::new(|ui, handle, evt| {
if evt.source == handle || ui.is_node_child_of(evt.source, handle) {
let text_box = ui.get_node_mut(handle).as_text_box_mut();
match evt.kind {
UIEventKind::Text { symbol } => {
text_box.insert_char(symbol);
}
UIEventKind::KeyDown { code } => {
match code {
VirtualKeyCode::Up => {
text_box.move_caret_y(1, VerticalDirection::Up);
}
VirtualKeyCode::Down => {
text_box.move_caret_y(1, VerticalDirection::Down);
}
VirtualKeyCode::Right => {
text_box.move_caret_x(1, HorizontalDirection::Right);
}
VirtualKeyCode::Left => {
text_box.move_caret_x(1, HorizontalDirection::Left);
}
VirtualKeyCode::Delete => {
text_box.remove_char(HorizontalDirection::Right);
}
VirtualKeyCode::Back => {
text_box.remove_char(HorizontalDirection::Left);
}
_ => ()
}
}
UIEventKind::MouseDown { pos, button } => {
if button == MouseButton::Left {
text_box.selection_range = None;
text_box.selecting = true;
if let Some(position) = text_box.screen_pos_to_text_pos(pos) {
text_box.caret_line = position.line;
text_box.caret_offset = position.offset;
text_box.selection_range = Some(SelectionRange {
begin: position,
end: position,
})
}
ui.capture_mouse(handle);
}
}
UIEventKind::MouseMove { pos } => {
if text_box.selecting {
if let Some(position) = text_box.screen_pos_to_text_pos(pos) {
if let Some(ref mut sel_range) = text_box.selection_range {
sel_range.end = position;
}
}
}
}
UIEventKind::MouseUp { .. } => {
text_box.selecting = false;
ui.release_mouse_capture();
}
_ => {}
}
}
}))
.build(),
caret_line: 0,
caret_offset: 0,
caret_visible: true,
blink_timer: 0.0,
blink_interval: 0.5,
formatted_text: FormattedTextBuilder::new()
.with_text(self.text)
.with_font(self.font.unwrap_or(ui.get_default_font()))
.build(),
selection_range: None,
selecting: false,
});
ui.add_node(text_box)
}
}
+11 -2
View File
@@ -19,6 +19,7 @@ use crate::gui::{
node::UINode,
event::{UIEventHandler, UIEvent},
draw::DrawingContext,
Update
};
use std::cell::RefCell;
@@ -78,6 +79,12 @@ impl Default for Widget {
}
}
impl Update for Widget {
fn update(&mut self, _dt: f32) {
}
}
impl Layout for Widget {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
let mut size = Vec2::ZERO;
@@ -277,8 +284,10 @@ impl WidgetBuilder {
self
}
pub fn with_children(mut self, mut children: Vec<Handle<UINode>>) -> Self {
self.children.append(&mut children);
pub fn with_children(mut self, children: &[Handle<UINode>]) -> Self {
for child in children {
self.children.push(*child)
}
self
}
+8 -1
View File
@@ -17,7 +17,8 @@ use crate::gui::{
Layout,
widget::{Widget, WidgetBuilder, AsWidget},
Draw,
draw::DrawingContext
draw::DrawingContext,
Update
};
/// Represents a widget looking as window in Windows - with title, minimize and close buttons.
@@ -40,6 +41,12 @@ impl AsWidget for Window {
}
}
impl Update for Window {
fn update(&mut self, dt: f32) {
self.widget.update(dt)
}
}
impl Layout for Window {
fn measure_override(&self, ui: &UserInterface, available_size: Vec2) -> Vec2 {
self.widget.measure_override(ui, available_size)
+6
View File
@@ -950,6 +950,12 @@ impl Font {
(1.05 * area.sqrt()) as i32
}
#[inline]
pub fn get_glyph_advance(&self, c: u32) -> f32 {
self.get_glyph(c)
.map_or(self.get_height(), |glyph| glyph.get_advance())
}
fn pack(&mut self) {
self.atlas_size = self.compute_atlas_size();