mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Initial client implementation of tab completion
This commit is contained in:
parent
b0f38591cd
commit
cdfcdffbf8
@ -1312,4 +1312,3 @@ fn handle_version(
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,9 +8,12 @@ use common::{msg::validate_chat_msg, ChatType};
|
|||||||
use conrod_core::{
|
use conrod_core::{
|
||||||
input::Key,
|
input::Key,
|
||||||
position::Dimension,
|
position::Dimension,
|
||||||
text::cursor::Index,
|
text::{
|
||||||
|
self,
|
||||||
|
cursor::{self, Index},
|
||||||
|
},
|
||||||
widget::{self, Button, Id, List, Rectangle, Text, TextEdit},
|
widget::{self, Button, Id, List, Rectangle, Text, TextEdit},
|
||||||
widget_ids, Colorable, Positionable, Sizeable, UiCell, Widget, WidgetCommon,
|
widget_ids, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon,
|
||||||
};
|
};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@ -18,13 +21,16 @@ widget_ids! {
|
|||||||
struct Ids {
|
struct Ids {
|
||||||
message_box,
|
message_box,
|
||||||
message_box_bg,
|
message_box_bg,
|
||||||
input,
|
chat_input,
|
||||||
input_bg,
|
chat_input_bg,
|
||||||
chat_arrow,
|
chat_arrow,
|
||||||
|
completion_box,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_MESSAGES: usize = 100;
|
const MAX_MESSAGES: usize = 100;
|
||||||
|
// Maximum completions shown at once
|
||||||
|
const MAX_COMPLETIONS: usize = 10;
|
||||||
|
|
||||||
#[derive(WidgetCommon)]
|
#[derive(WidgetCommon)]
|
||||||
pub struct Chat<'a> {
|
pub struct Chat<'a> {
|
||||||
@ -64,6 +70,9 @@ impl<'a> Chat<'a> {
|
|||||||
|
|
||||||
pub fn input(mut self, input: String) -> Self {
|
pub fn input(mut self, input: String) -> Self {
|
||||||
if let Ok(()) = validate_chat_msg(&input) {
|
if let Ok(()) = validate_chat_msg(&input) {
|
||||||
|
if input.contains('\t') {
|
||||||
|
println!("Contains tab: '{}'", input);
|
||||||
|
}
|
||||||
self.force_input = Some(input);
|
self.force_input = Some(input);
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
@ -97,6 +106,10 @@ pub struct State {
|
|||||||
// Index into the history Vec, history_pos == 0 is history not in use
|
// Index into the history Vec, history_pos == 0 is history not in use
|
||||||
// otherwise index is history_pos -1
|
// otherwise index is history_pos -1
|
||||||
history_pos: usize,
|
history_pos: usize,
|
||||||
|
completions: Vec<String>,
|
||||||
|
// Index into the completion Vec, completions_pos == 0 means not in use
|
||||||
|
// otherwise index is completions_pos -1
|
||||||
|
completions_pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
@ -115,6 +128,8 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
messages: VecDeque::new(),
|
messages: VecDeque::new(),
|
||||||
history: VecDeque::new(),
|
history: VecDeque::new(),
|
||||||
history_pos: 0,
|
history_pos: 0,
|
||||||
|
completions: Vec::new(),
|
||||||
|
completions_pos: 0,
|
||||||
ids: Ids::new(id_gen),
|
ids: Ids::new(id_gen),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,51 +155,77 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
let mut force_cursor = self.force_cursor;
|
let mut force_cursor = self.force_cursor;
|
||||||
|
|
||||||
// If up or down are pressed move through history
|
// If up or down are pressed move through history
|
||||||
match ui.widget_input(state.ids.input).presses().key().fold(
|
let history_move =
|
||||||
(false, false),
|
ui.widget_input(state.ids.chat_input)
|
||||||
|(up, down), key_press| match key_press.key {
|
.presses()
|
||||||
Key::Up => (true, down),
|
.key()
|
||||||
Key::Down => (up, true),
|
.fold(0, |n, key_press| match key_press.key {
|
||||||
_ => (up, down),
|
Key::Up => n + 1,
|
||||||
},
|
Key::Down => n - 1,
|
||||||
) {
|
_ => n,
|
||||||
(true, false) => {
|
|
||||||
if state.history_pos < state.history.len() {
|
|
||||||
state.update(|s| {
|
|
||||||
s.history_pos += 1;
|
|
||||||
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
|
|
||||||
force_cursor = Some(Index{line: 0, char: s.input.len()});
|
|
||||||
});
|
});
|
||||||
}
|
if history_move != 0 {
|
||||||
},
|
|
||||||
(false, true) => {
|
|
||||||
if state.history_pos > 0 {
|
|
||||||
state.update(|s| {
|
state.update(|s| {
|
||||||
|
if history_move > 0 {
|
||||||
|
if s.history_pos < s.history.len() {
|
||||||
|
s.history_pos += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if s.history_pos > 0 {
|
||||||
s.history_pos -= 1;
|
s.history_pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.history_pos > 0 {
|
if s.history_pos > 0 {
|
||||||
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
|
s.input = s.history.get(s.history_pos - 1).unwrap().to_owned();
|
||||||
force_cursor = Some(Index{line: 0, char: s.input.len()});
|
force_cursor =
|
||||||
|
cursor_offset_to_index(s.input.len(), &s.input, &ui, &self.fonts);
|
||||||
} else {
|
} else {
|
||||||
s.input.clear();
|
s.input.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
_ => {},
|
// Handle tab-completion
|
||||||
|
if let Some(cursor) = state.input.find('\t') {
|
||||||
|
state.update(|s| {
|
||||||
|
if s.completions_pos > 0 {
|
||||||
|
if s.completions_pos >= s.completions.len() {
|
||||||
|
s.completions_pos = 1;
|
||||||
|
} else {
|
||||||
|
s.completions_pos += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO FIXME pull completions from common::cmd
|
||||||
|
s.completions = "a,bc,def,ghi,jklm,nop,qr,stu,v,w,xyz"
|
||||||
|
.split(",")
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect();
|
||||||
|
s.completions_pos = 1;
|
||||||
|
}
|
||||||
|
//let index = force_cursor;
|
||||||
|
//let cursor = index.and_then(|index| cursor_index_to_offset(index, &s.input,
|
||||||
|
// ui, &self.fonts)).unwrap_or(0);
|
||||||
|
let replacement = &s.completions[s.completions_pos - 1];
|
||||||
|
let (completed, offset) = do_tab_completion(cursor, &s.input, replacement);
|
||||||
|
force_cursor = cursor_offset_to_index(offset, &completed, &ui, &self.fonts);
|
||||||
|
s.input = completed;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
|
let keyboard_capturer = ui.global_input().current.widget_capturing_keyboard;
|
||||||
|
|
||||||
if let Some(input) = &self.force_input {
|
if let Some(input) = &self.force_input {
|
||||||
state.update(|s| s.input = input.clone());
|
state.update(|s| s.input = input.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_focused =
|
let input_focused =
|
||||||
keyboard_capturer == Some(state.ids.input) || keyboard_capturer == Some(id);
|
keyboard_capturer == Some(state.ids.chat_input) || keyboard_capturer == Some(id);
|
||||||
|
|
||||||
// Only show if it has the keyboard captured.
|
// Only show if it has the keyboard captured.
|
||||||
// Chat input uses a rectangle as its background.
|
// Chat input uses a rectangle as its background.
|
||||||
if input_focused {
|
if input_focused {
|
||||||
|
// Any changes to this TextEdit's width and font size must be reflected in
|
||||||
|
// `cursor_offset_to_index` below.
|
||||||
let mut text_edit = TextEdit::new(&state.input)
|
let mut text_edit = TextEdit::new(&state.input)
|
||||||
.w(460.0)
|
.w(460.0)
|
||||||
.restrict_to_height(false)
|
.restrict_to_height(false)
|
||||||
@ -205,15 +246,18 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
.rgba(0.0, 0.0, 0.0, transp + 0.1)
|
.rgba(0.0, 0.0, 0.0, transp + 0.1)
|
||||||
.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
||||||
.w(470.0)
|
.w(470.0)
|
||||||
.set(state.ids.input_bg, ui);
|
.set(state.ids.chat_input_bg, ui);
|
||||||
|
|
||||||
if let Some(str) = text_edit
|
if let Some(str) = text_edit
|
||||||
.top_left_with_margins_on(state.ids.input_bg, 1.0, 1.0)
|
.top_left_with_margins_on(state.ids.chat_input_bg, 1.0, 1.0)
|
||||||
.set(state.ids.input, ui)
|
.set(state.ids.chat_input, ui)
|
||||||
{
|
{
|
||||||
let mut input = str.to_owned();
|
let mut input = str.to_owned();
|
||||||
input.retain(|c| c != '\n');
|
input.retain(|c| c != '\n');
|
||||||
if let Ok(()) = validate_chat_msg(&input) {
|
if let Ok(()) = validate_chat_msg(&input) {
|
||||||
|
if input.contains('\t') {
|
||||||
|
println!("Contains tab: '{}'", input);
|
||||||
|
}
|
||||||
state.update(|s| s.input = input);
|
state.update(|s| s.input = input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,7 +268,7 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
.rgba(0.0, 0.0, 0.0, transp)
|
.rgba(0.0, 0.0, 0.0, transp)
|
||||||
.and(|r| {
|
.and(|r| {
|
||||||
if input_focused {
|
if input_focused {
|
||||||
r.up_from(state.ids.input_bg, 0.0)
|
r.up_from(state.ids.chat_input_bg, 0.0)
|
||||||
} else {
|
} else {
|
||||||
r.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
r.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
|
||||||
}
|
}
|
||||||
@ -304,11 +348,11 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
// If the chat widget is focused, return a focus event to pass the focus to the
|
// If the chat widget is focused, return a focus event to pass the focus to the
|
||||||
// input box.
|
// input box.
|
||||||
if keyboard_capturer == Some(id) {
|
if keyboard_capturer == Some(id) {
|
||||||
Some(Event::Focus(state.ids.input))
|
Some(Event::Focus(state.ids.chat_input))
|
||||||
}
|
}
|
||||||
// If enter is pressed and the input box is not empty, send the current message.
|
// If enter is pressed and the input box is not empty, send the current message.
|
||||||
else if ui
|
else if ui
|
||||||
.widget_input(state.ids.input)
|
.widget_input(state.ids.chat_input)
|
||||||
.presses()
|
.presses()
|
||||||
.key()
|
.key()
|
||||||
.any(|key_press| match key_press.key {
|
.any(|key_press| match key_press.key {
|
||||||
@ -333,3 +377,79 @@ impl<'a> Widget for Chat<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize) {
|
||||||
|
let mut pre_ws = None;
|
||||||
|
let mut post_ws = None;
|
||||||
|
for (char_i, (byte_i, c)) in input.char_indices().enumerate() {
|
||||||
|
if c.is_whitespace() && c != '\t' {
|
||||||
|
if char_i < cursor {
|
||||||
|
pre_ws = Some(byte_i);
|
||||||
|
} else {
|
||||||
|
assert_eq!(post_ws, None); // TODO debug
|
||||||
|
post_ws = Some(byte_i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match (pre_ws, post_ws) {
|
||||||
|
(None, None) => (word.to_string(), word.chars().count()),
|
||||||
|
(None, Some(i)) => (
|
||||||
|
format!("{}{}", word, input.split_at(i).1),
|
||||||
|
word.chars().count(),
|
||||||
|
),
|
||||||
|
(Some(i), None) => {
|
||||||
|
let l_split = input.split_at(i).0;
|
||||||
|
let completed = format!("{} {}", l_split, word);
|
||||||
|
(
|
||||||
|
completed,
|
||||||
|
l_split.chars().count() + 1 + word.chars().count(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(Some(i), Some(j)) => {
|
||||||
|
let l_split = input.split_at(i).0;
|
||||||
|
let r_split = input.split_at(j).1;
|
||||||
|
let completed = format!("{} {}{}", l_split, word, r_split);
|
||||||
|
(
|
||||||
|
completed,
|
||||||
|
l_split.chars().count() + 1 + word.chars().count(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_index_to_offset(
|
||||||
|
index: text::cursor::Index,
|
||||||
|
text: &str,
|
||||||
|
ui: &Ui,
|
||||||
|
fonts: &ConrodVoxygenFonts,
|
||||||
|
) -> Option<usize> {
|
||||||
|
// Width and font must match that of the chat TextEdit
|
||||||
|
let width = 460.0;
|
||||||
|
let font = ui.fonts.get(fonts.opensans.conrod_id)?;
|
||||||
|
let font_size = fonts.opensans.scale(15);
|
||||||
|
let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
|
||||||
|
|
||||||
|
text::glyph::index_after_cursor(infos, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_offset_to_index(
|
||||||
|
offset: usize,
|
||||||
|
text: &str,
|
||||||
|
ui: &Ui,
|
||||||
|
fonts: &ConrodVoxygenFonts,
|
||||||
|
) -> Option<Index> {
|
||||||
|
// This moves the cursor to the given offset. Conrod is a pain.
|
||||||
|
//let iter = cursor::xys_per_line_from_text(&text, &[], &font, font_size,
|
||||||
|
// Justify::Left, Align::Start, 2.0, Rect{x: Range{start: 0.0, end: width}, y:
|
||||||
|
// Range{start: 0.0, end: 12.345}});
|
||||||
|
// cursor::closest_cursor_index_and_xy([f64::MAX, f64::MAX], iter).map(|(i, _)|
|
||||||
|
// i) Width and font must match that of the chat TextEdit
|
||||||
|
let width = 460.0;
|
||||||
|
let font = ui.fonts.get(fonts.opensans.conrod_id)?;
|
||||||
|
let font_size = fonts.opensans.scale(15);
|
||||||
|
let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width);
|
||||||
|
|
||||||
|
cursor::index_before_char(infos, offset)
|
||||||
|
}
|
||||||
|
@ -2296,6 +2296,27 @@ impl Hud {
|
|||||||
camera: &Camera,
|
camera: &Camera,
|
||||||
dt: Duration,
|
dt: Duration,
|
||||||
) -> Vec<Event> {
|
) -> Vec<Event> {
|
||||||
|
// conrod eats tabs. Un-eat a tabstop so tab completion can work
|
||||||
|
if self.ui.ui.global_input().events().any(|event| {
|
||||||
|
use conrod_core::{event, input};
|
||||||
|
match event {
|
||||||
|
//event::Event::Raw(event::Input::Press(input::Button::Keyboard(input::Key::Tab)))
|
||||||
|
// => true,
|
||||||
|
event::Event::Ui(event::Ui::Press(
|
||||||
|
_,
|
||||||
|
event::Press {
|
||||||
|
button: event::Button::Keyboard(input::Key::Tab),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
self.ui
|
||||||
|
.ui
|
||||||
|
.handle_event(conrod_core::event::Input::Text("\t".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(maybe_id) = self.to_focus.take() {
|
if let Some(maybe_id) = self.to_focus.take() {
|
||||||
self.ui.focus_widget(maybe_id);
|
self.ui.focus_widget(maybe_id);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user