use {
Align,
Backend,
CharacterCache,
Color,
Colorable,
FontSize,
GlyphCache,
IndexSlot,
Line,
NodeIndex,
Point,
Positionable,
Range,
Rect,
Rectangle,
Scalar,
Sizeable,
Text,
Widget,
};
use event;
use input;
use std;
use text;
use utils;
use widget;
use widget::primitive::text::Wrap;
/// A widget for displaying and mutating multi-line text, given as a `String`.
///
/// By default the text is wrapped via the first whitespace before the line exceeds the
/// `TextEdit`'s width, however a user may change this using the `.wrap_by_character` method.
pub struct TextEdit<'a, F> {
common: widget::CommonBuilder,
text: &'a mut String,
/// The reaction for the TextEdit.
pub maybe_react: Option<F>,
style: Style,
}
/// Unique kind for the widget type.
pub const KIND: widget::Kind = "TextEdit";
widget_style!{
KIND;
/// Unique graphical styling for the TextEdit.
style Style {
/// The color of the text (this includes cursor and selection color).
- color: Color { theme.shape_color }
/// The font size for the text.
- font_size: FontSize { theme.font_size_medium }
/// The horizontal alignment of the text.
- x_align: Align { Align::Start }
/// The vertical alignment of the text.
- y_align: Align { Align::End }
/// The vertical space between each line of text.
- line_spacing: Scalar { 1.0 }
/// The way in which text is wrapped at the end of a line.
- line_wrap: Wrap { Wrap::Whitespace }
/// Do not allow to enter text that would exceed the bounds of the `TextEdit`'s `Rect`.
- restrict_to_height: bool { true }
}
}
/// The State of the TextEdit widget that will be cached within the Ui.
#[derive(Clone, Debug, PartialEq)]
pub struct State {
cursor: Cursor,
/// Track whether some sort of dragging is currently occurring.
drag: Option<Drag>,
/// Information about each line of text.
line_infos: Vec<text::line::Info>,
selected_rectangle_indices: Vec<NodeIndex>,
rectangle_idx: IndexSlot,
text_idx: IndexSlot,
cursor_idx: IndexSlot,
highlight_idx: IndexSlot,
}
/// Track whether some sort of dragging is currently occurring.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Drag {
Selecting,
#[allow(dead_code)] // TODO: Implement this.
MoveSelection,
}
/// The position of the `Cursor` over the text.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Cursor {
/// The cursor is at the given character index.
Idx(text::cursor::Index),
/// The cursor is a selection between these two indices.
///
/// The `start` is always the "anchor" point.
///
/// The `end` may be either greater or less than the `start`.
Selection {
start: text::cursor::Index,
end: text::cursor::Index,
},
}
impl<'a, F> TextEdit<'a, F> {
/// Construct a TextEdit widget.
pub fn new(text: &'a mut String) -> Self {
TextEdit {
common: widget::CommonBuilder::new(),
text: text,
maybe_react: None,
style: Style::new(),
}
}
/// The `TextEdit` will wrap text via the whitespace that precedes the first width-exceeding
/// character.
///
/// This is the default setting.
pub fn wrap_by_whitespace(self) -> Self {
self.line_wrap(Wrap::Whitespace)
}
/// By default, the `TextEdit` will wrap text via the whitespace that precedes the first
/// width-exceeding character.
///
/// Calling this method causes the `TextEdit` to wrap text at the first exceeding character.
pub fn wrap_by_character(self) -> Self {
self.line_wrap(Wrap::Character)
}
/// Align the text to the left of its bounding **Rect**'s *x* axis range.
pub fn align_text_left(self) -> Self {
self.x_align_text(Align::Start)
}
/// Align the text to the middle of its bounding **Rect**'s *x* axis range.
pub fn align_text_x_middle(self) -> Self {
self.x_align_text(Align::Middle)
}
/// Align the text to the right of its bounding **Rect**'s *x* axis range.
pub fn align_text_right(self) -> Self {
self.x_align_text(Align::End)
}
/// Align the text to the left of its bounding **Rect**'s *y* axis range.
pub fn align_text_bottom(self) -> Self {
self.y_align_text(Align::Start)
}
/// Align the text to the middle of its bounding **Rect**'s *y* axis range.
pub fn align_text_y_middle(self) -> Self {
self.y_align_text(Align::Middle)
}
/// Align the text to the right of its bounding **Rect**'s *y* axis range.
pub fn align_text_top(self) -> Self {
self.y_align_text(Align::End)
}
/// Align the text to the middle of its bounding **Rect**.
pub fn align_text_middle(self) -> Self {
self.align_text_x_middle().align_text_y_middle()
}
builder_methods!{
pub font_size { style.font_size = Some(FontSize) }
pub react { maybe_react = Some(F) }
pub x_align_text { style.x_align = Some(Align) }
pub y_align_text { style.y_align = Some(Align) }
pub line_wrap { style.line_wrap = Some(Wrap) }
pub line_spacing { style.line_spacing = Some(Scalar) }
pub restrict_to_height { style.restrict_to_height = Some(bool) }
}
}
impl<'a, F> Widget for TextEdit<'a, F>
where F: FnMut(&mut String),
{
type State = State;
type Style = Style;
fn common(&self) -> &widget::CommonBuilder {
&self.common
}
fn common_mut(&mut self) -> &mut widget::CommonBuilder {
&mut self.common
}
fn unique_kind(&self) -> &'static str {
KIND
}
fn init_state(&self) -> State {
State {
cursor: Cursor::Idx(text::cursor::Index { line: 0, char: 0 }),
drag: None,
line_infos: Vec::new(),
selected_rectangle_indices: Vec::new(),
rectangle_idx: IndexSlot::new(),
text_idx: IndexSlot::new(),
cursor_idx: IndexSlot::new(),
highlight_idx: IndexSlot::new(),
}
}
fn style(&self) -> Style {
self.style.clone()
}
/// Update the state of the TextEdit.
fn update<B: Backend>(self, args: widget::UpdateArgs<Self, B>) {
let widget::UpdateArgs { idx, state, rect, style, mut ui, .. } = args;
let TextEdit { text, .. } = self;
let font_size = style.font_size(ui.theme());
let line_wrap = style.line_wrap(ui.theme());
let x_align = style.x_align(ui.theme());
let y_align = style.y_align(ui.theme());
let line_spacing = style.line_spacing(ui.theme());
let restrict_to_height = style.restrict_to_height(ui.theme());
let text_idx = state.text_idx.get(&mut ui);
/// Returns an iterator yielding the `text::line::Info` for each line in the given text
/// with the given styling.
type LineInfos<'a, C> = text::line::Infos<'a, C, text::line::NextBreakFnPtr<C>>;
fn line_infos<'a, C>(text: &'a str,
glyph_cache: &'a GlyphCache<C>,
font_size: FontSize,
line_wrap: Wrap,
max_width: Scalar) -> LineInfos<'a, C>
where C: CharacterCache,
{
let infos = text::line::infos(text, glyph_cache, font_size);
match line_wrap {
Wrap::Whitespace => infos.wrap_by_whitespace(max_width),
Wrap::Character => infos.wrap_by_character(max_width),
}
}
// Check to see if the given text has changed since the last time the widget was updated.
{
let maybe_new_line_infos = {
let line_info_slice = &state.line_infos[..];
let new_line_infos =
line_infos(text, ui.glyph_cache(), font_size, line_wrap, rect.w());
match utils::write_if_different(line_info_slice, new_line_infos) {
std::borrow::Cow::Owned(new) => Some(new),
_ => None,
}
};
if let Some(new_line_infos) = maybe_new_line_infos {
state.update(|state| state.line_infos = new_line_infos);
}
}
// Find the closest cursor index to the given `xy` position.
//
// Returns `None` if the given `text` is empty.
let closest_cursor_index_and_xy = |xy: Point,
text: &str,
line_infos: &[text::line::Info],
glyph_cache: &GlyphCache<B::CharacterCache>|
-> Option<(text::cursor::Index, Point)>
{
let line_infos = line_infos.iter().cloned();
let lines = line_infos.clone().map(|info| &text[info.byte_range()]);
let line_rects = text::line::rects(line_infos.clone(), font_size, rect,
x_align, y_align, line_spacing);
let lines_with_rects = lines.zip(line_rects.clone());
// Find the index of the line that is closest on the *y* axis.
let mut xys_per_line_enumerated =
text::cursor::xys_per_line(lines_with_rects, glyph_cache, font_size).enumerate();
xys_per_line_enumerated.next().and_then(|(first_line_idx, (_, first_line_y))| {
let mut closest_line_idx = first_line_idx;
let mut closest_diff = (xy[1] - first_line_y.middle()).abs();
for (line_idx, (_, line_y)) in xys_per_line_enumerated {
if line_y.is_over(xy[1]) {
closest_line_idx = line_idx;
break;
} else {
let diff = (xy[1] - line_y.middle()).abs();
if diff < closest_diff {
closest_line_idx = line_idx;
closest_diff = diff;
} else {
break;
}
}
}
// Find the index of the cursor position along the closest line.
let lines_with_rects = line_infos.map(|info| &text[info.byte_range()]).zip(line_rects);
text::cursor::xys_per_line(lines_with_rects, glyph_cache, font_size)
.nth(closest_line_idx)
.map(|(xs, line_y)| {
let mut xs_enumerated = xs.enumerate();
// `xs` always yields at least one `x` (the start of the line).
let (first_idx, first_x) = xs_enumerated.next().unwrap();
let first_diff = (xy[0] - first_x).abs();
let mut closest_idx = first_idx;
let mut closest_x = first_x;
let mut closest_diff = first_diff;
for (i, x) in xs_enumerated {
let diff = (xy[0] - x).abs();
if diff < closest_diff {
closest_idx = i;
closest_x = x;
closest_diff = diff;
} else {
break;
}
}
let index = text::cursor::Index { line: closest_line_idx, char: closest_idx };
let point = [closest_x, line_y.middle()];
(index, point)
})
})
};
let mut cursor = state.cursor;
let mut drag = state.drag;
// Check for the following events:
// - `Text` events for receiving new text.
// - Left mouse `Press` events for either:
// - setting the cursor or start of a selection.
// - begin dragging selected text.
// - Left mouse `Drag` for extending the end of the selection, or for dragging selected text.
'events: for widget_event in ui.widget_input(idx).events() {
match widget_event {
event::Widget::Press(press) => match press.button {
// If the left mouse button was pressed, place a `Cursor` with the starting
// index at the mouse position.
event::Button::Mouse(input::MouseButton::Left, rel_xy) => {
let abs_xy = utils::vec2_add(rel_xy, rect.xy());
let infos = &state.line_infos;
let cache = ui.glyph_cache();
let closest = closest_cursor_index_and_xy(abs_xy, text, infos, cache);
if let Some((closest_cursor, _)) = closest {
cursor = Cursor::Idx(closest_cursor);
}
// TODO: Differentiate between Selecting and MoveSelection.
drag = Some(Drag::Selecting);
}
// Check for control keys.
event::Button::Keyboard(key) => match key {
// If `Cursor::Idx`, remove the `char` behind the cursor.
// If `Cursor::Selection`, remove the selected text.
input::Key::Backspace => {
match cursor {
Cursor::Idx(cursor_idx) => {
let idx_after_cursor = {
let line_infos = state.line_infos.iter().cloned();
text::char::index_after_cursor(line_infos, cursor_idx)
};
if let Some(idx) = idx_after_cursor {
let idx_to_remove = idx - 1;
let new_cursor_idx = {
let line_infos = state.line_infos.iter().cloned();
text::cursor::index_before_char(line_infos, idx_to_remove)
};
if let Some(new_cursor_idx) = new_cursor_idx {
cursor = Cursor::Idx(new_cursor_idx);
*text = text.chars().take(idx_to_remove)
.chain(text.chars().skip(idx))
.collect();
state.update(|state| {
state.line_infos =
line_infos(text, ui.glyph_cache(), font_size,
line_wrap, rect.w()).collect();
});
}
}
},
Cursor::Selection { start, end } => {
let (start_idx, end_idx) = {
let line_infos = state.line_infos.iter().cloned();
(text::char::index_after_cursor(line_infos.clone(), start)
.expect("text::cursor::Index was out of range"),
text::char::index_after_cursor(line_infos, end)
.expect("text::cursor::Index was out of range"))
};
let (start_idx, end_idx) =
if start_idx <= end_idx { (start_idx, end_idx) }
else { (end_idx, start_idx) };
let new_cursor_char_idx =
if start_idx > 0 { start_idx } else { 0 };
let new_cursor_idx = {
let line_infos = state.line_infos.iter().cloned();
text::cursor::index_before_char(line_infos, new_cursor_char_idx)
.expect("char index was out of range")
};
cursor = Cursor::Idx(new_cursor_idx);
*text = text.chars().take(start_idx)
.chain(text.chars().skip(end_idx))
.collect();
state.update(|state| {
state.line_infos =
line_infos(text, ui.glyph_cache(), font_size,
line_wrap, rect.w()).collect();
});
},
}
},
input::Key::Left => {
if !press.modifiers.contains(input::keyboard::CTRL) {
match cursor {
// Move the cursor to the previous position.
Cursor::Idx(cursor_idx) => {
let new_cursor_idx = {
let line_infos = state.line_infos.iter().cloned();
cursor_idx.previous(line_infos).unwrap_or(cursor_idx)
};
cursor = Cursor::Idx(new_cursor_idx);
},
// Move the cursor to the start of the current selection.
Cursor::Selection { start, end } => {
let new_cursor_idx = std::cmp::min(start, end);
cursor = Cursor::Idx(new_cursor_idx);
},
}
}
},
input::Key::Right => {
if !press.modifiers.contains(input::keyboard::CTRL) {
match cursor {
// Move the cursor to the next position.
Cursor::Idx(cursor_idx) => {
let new_cursor_idx = {
let line_infos = state.line_infos.iter().cloned();
cursor_idx.next(line_infos).unwrap_or(cursor_idx)
};
cursor = Cursor::Idx(new_cursor_idx);
},
// Move the cursor to the end of the current selection.
Cursor::Selection { start, end } => {
let new_cursor_idx = std::cmp::max(start, end);
cursor = Cursor::Idx(new_cursor_idx);
},
}
}
},
input::Key::Up => {
},
input::Key::Down => {
},
input::Key::A => {
// Select all text on Ctrl+a.
if press.modifiers.contains(input::keyboard::CTRL) {
let start = text::cursor::Index { line: 0, char: 0 };
let end = {
let line_infos = state.line_infos.iter().cloned();
text::cursor::index_before_char(line_infos, text.chars().count())
.expect("char index was out of range")
};
cursor = Cursor::Selection { start: start, end: end };
}
},
input::Key::E => {
// If cursor is `Idx`, move cursor to end.
if press.modifiers.contains(input::keyboard::CTRL) {
}
},
_ => (),
},
_ => (),
},
event::Widget::Release(release) => {
// Release drag.
if let event::Button::Mouse(input::MouseButton::Left, _) = release.button {
drag = None;
}
},
event::Widget::Text(event::Text { string, modifiers }) => {
if modifiers.contains(input::keyboard::CTRL)
|| string.chars().count() == 0
|| string.chars().next().is_none() {
continue 'events;
}
// Ignore text produced by arrow keys.
//
// TODO: These just happened to be the modifiers for the arrows on OS X, I've
// no idea if they also apply to other platforms. We should definitely see if
// there's a better way to handle this, or whether this should be fixed
// upstream.
match &string[..] {
"\u{f700}" | "\u{f701}" | "\u{f702}" | "\u{f703}" => continue 'events,
_ => ()
}
let string_char_count = string.chars().count();
// Construct the new text with the new string inserted at the cursor.
let (new_text, new_cursor_char_idx): (String, usize) = {
let (cursor_start, cursor_end) = match cursor {
Cursor::Idx(idx) => (idx, idx),
Cursor::Selection { start, end } =>
(std::cmp::min(start, end), std::cmp::max(start, end)),
};
let line_infos = state.line_infos.iter().cloned();
let (start_idx, end_idx) =
(text::char::index_after_cursor(line_infos.clone(), cursor_start)
.unwrap_or(0),
text::char::index_after_cursor(line_infos.clone(), cursor_end)
.unwrap_or(0));
let new_cursor_char_idx = start_idx + string_char_count;
let new_text = text.chars().take(start_idx)
.chain(string.chars())
.chain(text.chars().skip(end_idx))
.collect();
(new_text, new_cursor_char_idx)
};
// Calculate the new `line_infos` for the `new_text`.
let new_line_infos: Vec<_> =
line_infos(&new_text, ui.glyph_cache(), font_size, line_wrap, rect.w())
.collect();
// Check that the new text would not exceed the `inner_rect` bounds.
let num_lines = new_line_infos.len();
let height = text::height(num_lines, font_size, line_spacing);
if height < rect.h() || !restrict_to_height {
// Determine the new `Cursor` and its position.
let new_cursor_idx = {
let line_infos = new_line_infos.iter().cloned();
text::cursor::index_before_char(line_infos, new_cursor_char_idx)
.unwrap_or(text::cursor::Index {
line: 0,
char: string_char_count,
})
};
let new_cursor = Cursor::Idx(new_cursor_idx);
// Update the text, cursor and line_infos.
*text = new_text;
cursor = new_cursor;
state.update(|state| state.line_infos = new_line_infos);
}
},
// Check whether or not
event::Widget::Drag(drag_event) => {
if let input::MouseButton::Left = drag_event.button {
match drag {
Some(Drag::Selecting) => {
let start_cursor_idx = match cursor {
Cursor::Idx(idx) => idx,
Cursor::Selection { start, .. } => start,
};
let abs_xy = utils::vec2_add(drag_event.to, rect.xy());
let infos = &state.line_infos;
let cache = ui.glyph_cache();
match closest_cursor_index_and_xy(abs_xy, text, infos, cache) {
Some((end_cursor_idx, _)) =>
cursor = Cursor::Selection {
start: start_cursor_idx,
end: end_cursor_idx,
},
_ => (),
}
},
// TODO: This should move the selected text.
Some(Drag::MoveSelection) => {
unimplemented!();
},
None => (),
}
}
},
_ => (),
}
}
if state.cursor != cursor {
state.update(|state| state.cursor = cursor);
}
if state.drag != drag {
state.update(|state| state.drag = drag);
}
let color = style.color(ui.theme());
let font_size = style.font_size(ui.theme());
let num_lines = state.line_infos.iter().count();
let text_height = text::height(num_lines, font_size, line_spacing);
let text_y_range = Range::new(0.0, text_height).align_to(y_align, rect.y);
let text_rect = Rect { x: rect.x, y: text_y_range };
match line_wrap {
Wrap::Whitespace => Text::new(&self.text).wrap_by_word(),
Wrap::Character => Text::new(&self.text).wrap_by_character(),
}
.wh(text_rect.dim())
.xy(text_rect.xy())
.align_text_to(x_align)
.graphics_for(idx)
.color(color)
.line_spacing(line_spacing)
.font_size(font_size)
.set(text_idx, &mut ui);
// Draw the line for the cursor.
let cursor_idx = match cursor {
Cursor::Idx(idx) => idx,
Cursor::Selection { end, .. } => end,
};
// If this widget is not capturing the keyboard, no need to draw cursor or selection.
if ui.global_input().current.widget_capturing_keyboard != Some(idx) {
return;
}
// TODO: Simplify this block.
let (cursor_x, cursor_y_range) = {
let line_infos = state.line_infos.iter().cloned();
let lines = line_infos.clone().map(|info| &text[info.byte_range()]);
let line_rects = text::line::rects(line_infos.clone(), font_size, rect,
x_align, y_align, line_spacing);
let lines_with_rects = lines.zip(line_rects.clone());
let xys_per_line = text::cursor::xys_per_line(lines_with_rects, ui.glyph_cache(), font_size);
text::cursor::xy_at(xys_per_line, cursor_idx)
.unwrap_or_else(|| {
let x = rect.left();
let y = Range::new(0.0, font_size as Scalar).align_to(y_align, rect.y);
(x, y)
})
};
let cursor_line_idx = state.cursor_idx.get(&mut ui);
let start = [0.0, cursor_y_range.start];
let end = [0.0, cursor_y_range.end];
Line::centred(start, end)
.x_y(cursor_x, cursor_y_range.middle())
.graphics_for(idx)
.parent(idx)
.color(color)
.set(cursor_line_idx, &mut ui);
if let Cursor::Selection { start, end } = cursor {
let (start, end) = (std::cmp::min(start, end), std::cmp::max(start, end));
let selected_rects: Vec<Rect> = {
let line_infos = state.line_infos.iter().cloned();
let lines = line_infos.clone().map(|info| &text[info.byte_range()]);
let line_rects = text::line::rects(line_infos.clone(), font_size, rect,
x_align, y_align, line_spacing);
let lines_with_rects = lines.zip(line_rects.clone());
let cache = ui.glyph_cache();
text::line::selected_rects(lines_with_rects, cache, font_size, start, end)
.collect()
};
// Draw a semi-transparent `Rectangle` for the selected range across each line.
let selected_rect_color = color.highlighted().alpha(0.25);
for (i, selected_rect) in selected_rects.iter().enumerate() {
if i == state.selected_rectangle_indices.len() {
state.update(|state| {
state.selected_rectangle_indices.push(ui.new_unique_node_index());
});
}
let selected_rectangle_idx = state.selected_rectangle_indices[i];
Rectangle::fill(selected_rect.dim())
.xy(selected_rect.xy())
.color(selected_rect_color)
.graphics_for(idx)
.parent(idx)
.set(selected_rectangle_idx, &mut ui);
}
}
}
}
impl<'a, F> Colorable for TextEdit<'a, F> {
builder_method!(color { style.color = Some(Color) });
}