-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
use std::fmt::Display; | ||
|
||
use super::{EnterAction, InputField}; | ||
use crate::interface::builder::{Set, Unset}; | ||
use crate::interface::*; | ||
|
||
/// Type state [`InputField`] builder. This builder utilizes the type system to | ||
/// prevent calling the same method multiple times and calling | ||
/// [`build`](Self::build) before the mandatory methods have been called. | ||
#[must_use = "`build` needs to be called"] | ||
pub struct InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, HIDDEN, WIDTH> { | ||
input_state: STATE, | ||
ghost_text: TEXT, | ||
enter_action: ACTION, | ||
length: usize, | ||
hidden: bool, | ||
width_bound: Option<DimensionBound>, | ||
marker: PhantomData<(LENGTH, HIDDEN, WIDTH)>, | ||
} | ||
|
||
impl InputFieldBuilder<Unset, Unset, Unset, Unset, Unset, Unset> { | ||
pub fn new() -> Self { | ||
Self { | ||
input_state: Unset, | ||
ghost_text: Unset, | ||
enter_action: Unset, | ||
length: 0, | ||
hidden: false, | ||
width_bound: None, | ||
marker: PhantomData, | ||
} | ||
} | ||
} | ||
|
||
impl<TEXT, ACTION, LENGTH, HIDDEN, WIDTH> InputFieldBuilder<Unset, TEXT, ACTION, LENGTH, HIDDEN, WIDTH> { | ||
pub fn with_state(self, state: TrackedState<String>) -> InputFieldBuilder<TrackedState<String>, TEXT, ACTION, LENGTH, HIDDEN, WIDTH> { | ||
InputFieldBuilder { | ||
input_state: state, | ||
..self | ||
} | ||
} | ||
} | ||
|
||
impl<STATE, ACTION, LENGTH, HIDDEN, WIDTH> InputFieldBuilder<STATE, Unset, ACTION, LENGTH, HIDDEN, WIDTH> { | ||
pub fn with_ghost_text<TEXT: Display + 'static>( | ||
self, | ||
ghost_text: TEXT, | ||
) -> InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, HIDDEN, WIDTH> { | ||
InputFieldBuilder { ghost_text, ..self } | ||
} | ||
} | ||
|
||
impl<STATE, TEXT, LENGTH, HIDDEN, WIDTH> InputFieldBuilder<STATE, TEXT, Unset, LENGTH, HIDDEN, WIDTH> { | ||
pub fn with_enter_action( | ||
self, | ||
enter_action: impl FnMut() -> Vec<ClickAction> + 'static, | ||
) -> InputFieldBuilder<STATE, TEXT, EnterAction, LENGTH, HIDDEN, WIDTH> { | ||
InputFieldBuilder { | ||
enter_action: Box::new(enter_action), | ||
..self | ||
} | ||
} | ||
} | ||
|
||
impl<STATE, TEXT, ACTION, HIDDEN, WIDTH> InputFieldBuilder<STATE, TEXT, ACTION, Unset, HIDDEN, WIDTH> { | ||
pub fn with_length(self, length: usize) -> InputFieldBuilder<STATE, TEXT, ACTION, Set, HIDDEN, WIDTH> { | ||
InputFieldBuilder { | ||
length, | ||
marker: PhantomData, | ||
..self | ||
} | ||
} | ||
} | ||
|
||
impl<STATE, TEXT, ACTION, LENGTH, WIDTH> InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, Unset, WIDTH> { | ||
pub fn with_hidden(self) -> InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, Set, WIDTH> { | ||
InputFieldBuilder { | ||
hidden: true, | ||
marker: PhantomData, | ||
..self | ||
} | ||
} | ||
} | ||
|
||
impl<STATE, TEXT, ACTION, LENGTH, HIDDEN> InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, HIDDEN, Unset> { | ||
pub fn with_width_bound(self, width_bound: DimensionBound) -> InputFieldBuilder<STATE, TEXT, ACTION, LENGTH, HIDDEN, Set> { | ||
InputFieldBuilder { | ||
width_bound: Some(width_bound), | ||
marker: PhantomData, | ||
..self | ||
} | ||
} | ||
} | ||
|
||
impl<TEXT, HIDDEN, WIDTH> InputFieldBuilder<TrackedState<String>, TEXT, EnterAction, Set, HIDDEN, WIDTH> | ||
where | ||
TEXT: Display + 'static, | ||
{ | ||
/// Take the builder and turn it into a [`InputField`]. | ||
/// | ||
/// NOTE: This method is only available if [`with_state`](Self::with_state), | ||
/// [`with_ghost_text`](Self::with_ghost_text), | ||
/// [`with_enter_action`](Self::with_enter_action), | ||
/// and [`with_length`](Self::with_length) have been called on the builder. | ||
pub fn build(self) -> InputField<TEXT> { | ||
let Self { | ||
input_state, | ||
ghost_text, | ||
enter_action, | ||
length, | ||
hidden, | ||
width_bound, | ||
.. | ||
} = self; | ||
|
||
InputField { | ||
input_state, | ||
ghost_text, | ||
enter_action, | ||
length, | ||
hidden, | ||
width_bound, | ||
state: Default::default(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
mod builder; | ||
|
||
use std::fmt::Display; | ||
|
||
use procedural::dimension_bound; | ||
|
||
pub use self::builder::InputFieldBuilder; | ||
use crate::graphics::{InterfaceRenderer, Renderer}; | ||
use crate::input::MouseInputMode; | ||
use crate::interface::*; | ||
|
||
/// Local type alias to simplify the builder. | ||
type EnterAction = Box<dyn FnMut() -> Vec<ClickAction>>; | ||
|
||
pub struct InputField<TEXT: Display + 'static> { | ||
input_state: TrackedState<String>, | ||
ghost_text: TEXT, | ||
enter_action: EnterAction, | ||
length: usize, | ||
hidden: bool, | ||
width_bound: Option<DimensionBound>, | ||
state: ElementState, | ||
} | ||
|
||
impl<TEXT: Display + 'static> InputField<TEXT> { | ||
fn remove_character(&mut self) -> Vec<ClickAction> { | ||
self.input_state.with_mut(|input_state, changed| { | ||
if input_state.is_empty() { | ||
return Vec::new(); | ||
} | ||
|
||
input_state.pop(); | ||
changed(); | ||
|
||
vec![ClickAction::ChangeEvent(ChangeEvent::RENDER_WINDOW)] | ||
}) | ||
} | ||
|
||
fn add_character(&mut self, character: char) -> Vec<ClickAction> { | ||
self.input_state.with_mut(|input_state, changed| { | ||
if input_state.len() >= self.length { | ||
return Vec::new(); | ||
} | ||
|
||
input_state.push(character); | ||
changed(); | ||
|
||
vec![ClickAction::ChangeEvent(ChangeEvent::RENDER_WINDOW)] | ||
}) | ||
} | ||
} | ||
|
||
impl<TEXT: Display + 'static> Element for InputField<TEXT> { | ||
fn get_state(&self) -> &ElementState { | ||
&self.state | ||
} | ||
|
||
fn get_state_mut(&mut self) -> &mut ElementState { | ||
&mut self.state | ||
} | ||
|
||
fn resolve(&mut self, placement_resolver: &mut PlacementResolver, _interface_settings: &InterfaceSettings, theme: &InterfaceTheme) { | ||
let size_bound = self | ||
.width_bound | ||
.as_ref() | ||
.unwrap_or(&dimension_bound!(100%)) | ||
.add_height(theme.input.height_bound); | ||
|
||
self.state.resolve(placement_resolver, &size_bound); | ||
} | ||
|
||
fn hovered_element(&self, mouse_position: ScreenPosition, mouse_mode: &MouseInputMode) -> HoverInformation { | ||
match mouse_mode { | ||
MouseInputMode::None => self.state.hovered_element(mouse_position), | ||
_ => HoverInformation::Missed, | ||
} | ||
} | ||
|
||
fn left_click(&mut self, _update: &mut bool) -> Vec<ClickAction> { | ||
vec![ClickAction::FocusElement] | ||
} | ||
|
||
fn input_character(&mut self, character: char) -> Vec<ClickAction> { | ||
match character { | ||
'\u{8}' | '\u{7f}' => self.remove_character(), | ||
'\r' => (self.enter_action)(), | ||
character => self.add_character(character), | ||
} | ||
} | ||
|
||
fn render( | ||
&self, | ||
render_target: &mut <InterfaceRenderer as Renderer>::Target, | ||
renderer: &InterfaceRenderer, | ||
_state_provider: &StateProvider, | ||
interface_settings: &InterfaceSettings, | ||
theme: &InterfaceTheme, | ||
parent_position: ScreenPosition, | ||
screen_clip: ScreenClip, | ||
hovered_element: Option<&dyn Element>, | ||
focused_element: Option<&dyn Element>, | ||
_mouse_mode: &MouseInputMode, | ||
_second_theme: bool, | ||
) { | ||
let mut renderer = self | ||
.state | ||
.element_renderer(render_target, renderer, interface_settings, parent_position, screen_clip); | ||
|
||
let input_state = self.input_state.borrow(); | ||
let is_hovererd = self.is_element_self(hovered_element); | ||
let is_focused = self.is_element_self(focused_element); | ||
let text_offset = theme.input.text_offset.get(); | ||
|
||
let text = if input_state.is_empty() && !is_focused { | ||
self.ghost_text.to_string() | ||
} else if self.hidden { | ||
input_state.chars().map(|_| '*').collect() | ||
} else { | ||
input_state.clone() | ||
}; | ||
|
||
let background_color = if is_hovererd { | ||
theme.input.hovered_background_color.get() | ||
} else if is_focused { | ||
theme.input.focused_background_color.get() | ||
} else { | ||
theme.input.background_color.get() | ||
}; | ||
|
||
let text_color = if input_state.is_empty() && !is_focused { | ||
theme.input.ghost_text_color.get() | ||
} else if is_focused { | ||
theme.input.focused_text_color.get() | ||
} else { | ||
theme.input.text_color.get() | ||
}; | ||
|
||
renderer.render_background(theme.input.corner_radius.get(), background_color); | ||
renderer.render_text(&text, text_offset, text_color, theme.input.font_size.get()); | ||
|
||
if is_focused { | ||
let cursor_offset = (text_offset.left + theme.input.cursor_offset.get()) * interface_settings.scaling.get() | ||
+ renderer.get_text_dimensions(&text, theme.input.font_size.get(), f32::MAX).x; | ||
|
||
let cursor_position = ScreenPosition::only_left(cursor_offset); | ||
let cursor_size = ScreenSize { | ||
width: theme.input.cursor_width.get(), | ||
height: self.state.cached_size.height, | ||
}; | ||
|
||
renderer.render_rectangle( | ||
cursor_position, | ||
cursor_size, | ||
CornerRadius::default(), | ||
theme.input.text_color.get(), | ||
); | ||
} | ||
} | ||
} |