fix: better handling of secure inputs

This commit is contained in:
Ashley Wulber 2024-01-17 16:48:33 -05:00 committed by Jeremy Soller
parent 4e18199444
commit 994e93d6d2
3 changed files with 187 additions and 132 deletions

View file

@ -89,6 +89,7 @@ pub enum Message {
ClearAll, ClearAll,
CardsToggled(bool), CardsToggled(bool),
ColorPickerUpdate(ColorPickerUpdate), ColorPickerUpdate(ColorPickerUpdate),
Hidden,
} }
pub enum Output { pub enum Output {
@ -115,6 +116,7 @@ pub struct State {
cards: Vec<String>, cards: Vec<String>,
pub timeline: Rc<RefCell<Timeline>>, pub timeline: Rc<RefCell<Timeline>>,
pub color_picker_model: ColorPickerModel, pub color_picker_model: ColorPickerModel,
pub hidden: bool,
} }
impl Default for State { impl Default for State {
@ -162,6 +164,7 @@ impl Default for State {
], ],
timeline: Rc::new(RefCell::new(Default::default())), timeline: Rc::new(RefCell::new(Default::default())),
color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None), color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None),
hidden: false,
} }
} }
} }
@ -210,6 +213,9 @@ impl State {
Message::ColorPickerUpdate(u) => { Message::ColorPickerUpdate(u) => {
_ = self.color_picker_model.update::<Message>(u); _ = self.color_picker_model.update::<Message>(u);
} }
Message::Hidden => {
self.hidden = !self.hidden;
}
} }
None None
@ -470,9 +476,11 @@ impl State {
.padding(16) .padding(16)
.style(cosmic::theme::Container::Background) .style(cosmic::theme::Container::Background)
.into(), .into(),
text_input( cosmic::widget::text_input::secure_input(
"Type to search apps or type “?” for more options...", "Type to search apps or type “?” for more options...",
&self.entry_value, &self.entry_value,
Some(Message::Hidden),
self.hidden,
) )
.on_input(Message::InputChanged) .on_input(Message::InputChanged)
.size(20) .size(20)

View file

@ -228,7 +228,7 @@ where
.core() .core()
.single_instance .single_instance
.then(|| super::single_instance_subscription::<T>()) .then(|| super::single_instance_subscription::<T>())
.unwrap_or_else(|| Subscription::none()), .unwrap_or_else(Subscription::none),
]) ])
} }

View file

@ -476,7 +476,7 @@ where
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::new(self.is_secure))
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
@ -489,6 +489,12 @@ where
state.is_pasting = None; state.is_pasting = None;
state.dragging_state = None; state.dragging_state = None;
} }
if state.is_secure != self.is_secure {
state.is_secure = self.is_secure;
state.dirty = true;
}
let mut children: Vec<_> = self let mut children: Vec<_> = self
.leading_icon .leading_icon
.iter_mut() .iter_mut()
@ -522,13 +528,13 @@ where
) -> layout::Node { ) -> layout::Node {
let font = self.font.unwrap_or_else(|| renderer.default_font()); let font = self.font.unwrap_or_else(|| renderer.default_font());
if self.dnd_icon { if self.dnd_icon {
let state = tree.state.downcast_mut::<State>();
let limits = limits.width(Length::Shrink).height(Length::Shrink); let limits = limits.width(Length::Shrink).height(Length::Shrink);
let size = self.size.unwrap_or_else(|| renderer.default_size().0); let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let bounds = limits.max(); let bounds = limits.max();
let state = tree.state.downcast_mut::<State>();
let value_paragraph = &mut state.value; let value_paragraph = &mut state.value;
let v = self.value.to_string(); let v = self.value.to_string();
value_paragraph.update(Text { value_paragraph.update(Text {
@ -551,7 +557,7 @@ where
let size = limits.resolve(Size::new(width, height)); let size = limits.resolve(Size::new(width, height));
layout::Node::with_children(size, vec![layout::Node::new(size)]) layout::Node::with_children(size, vec![layout::Node::new(size)])
} else { } else {
layout( let res = layout(
renderer, renderer,
limits, limits,
self.width, self.width,
@ -566,7 +572,29 @@ where
self.helper_line_height, self.helper_line_height,
font, font,
tree, tree,
) );
// XXX not ideal, but we need to update the cache when is_secure changes
let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let line_height = self.line_height;
let state = tree.state.downcast_mut::<State>();
if state.dirty {
state.dirty = false;
let value = if self.is_secure {
self.value.secure()
} else {
self.value.clone()
};
replace_paragraph(
state,
Layout::new(&res),
&value,
font,
iced::Pixels(size),
line_height,
);
}
res
} }
} }
@ -630,6 +658,10 @@ where
) -> event::Status { ) -> event::Status {
let text_layout = self.text_layout(layout); let text_layout = self.text_layout(layout);
let mut index = 0; let mut index = 0;
let font = self.font.unwrap_or_else(|| renderer.default_font());
let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let line_height = self.line_height;
if let (Some(leading_icon), Some(tree)) = if let (Some(leading_icon), Some(tree)) =
(self.leading_icon.as_mut(), tree.children.get_mut(index)) (self.leading_icon.as_mut(), tree.children.get_mut(index))
{ {
@ -637,22 +669,13 @@ where
children.next(); children.next();
let leading_icon_layout = children.next().unwrap(); let leading_icon_layout = children.next().unwrap();
if cursor_position.is_over(leading_icon_layout.bounds()) { if cursor_position.is_over(leading_icon_layout.bounds())
return leading_icon.as_widget_mut().on_event( || matches!(
tree, event,
event.clone(), Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft)
leading_icon_layout, )
cursor_position, {
renderer, let res = leading_icon.as_widget_mut().on_event(
clipboard,
shell,
viewport,
);
} else if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft)
) {
leading_icon.as_widget_mut().on_event(
tree, tree,
event.clone(), event.clone(),
leading_icon_layout, leading_icon_layout,
@ -662,6 +685,9 @@ where
shell, shell,
viewport, viewport,
); );
if res == event::Status::Captured {
return res;
}
} }
index += 1; index += 1;
} }
@ -675,22 +701,13 @@ where
} }
let trailing_icon_layout = children.next().unwrap(); let trailing_icon_layout = children.next().unwrap();
if cursor_position.is_over(trailing_icon_layout.bounds()) { if cursor_position.is_over(trailing_icon_layout.bounds())
return trailing_icon.as_widget_mut().on_event( | matches!(
tree, event,
event.clone(), Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft)
trailing_icon_layout, )
cursor_position, {
renderer, let res = trailing_icon.as_widget_mut().on_event(
clipboard,
shell,
viewport,
);
} else if matches!(
event,
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft)
) {
trailing_icon.as_widget_mut().on_event(
tree, tree,
event.clone(), event.clone(),
trailing_icon_layout, trailing_icon_layout,
@ -700,6 +717,9 @@ where
shell, shell,
viewport, viewport,
); );
if res == event::Status::Captured {
return res;
}
} }
} }
@ -707,12 +727,11 @@ where
event, event,
text_layout.children().next().unwrap(), text_layout.children().next().unwrap(),
cursor_position, cursor_position,
renderer,
clipboard, clipboard,
shell, shell,
&mut self.value, &mut self.value,
self.size, size,
self.font, font,
self.is_secure, self.is_secure,
self.on_input.as_deref(), self.on_input.as_deref(),
self.on_paste.as_deref(), self.on_paste.as_deref(),
@ -722,7 +741,7 @@ where
self.dnd_icon, self.dnd_icon,
self.on_dnd_command_produced.as_deref(), self.on_dnd_command_produced.as_deref(),
self.surface_ids, self.surface_ids,
self.line_height, line_height,
layout, layout,
) )
} }
@ -1062,12 +1081,11 @@ pub fn update<'a, Message>(
event: Event, event: Event,
text_layout: Layout<'_>, text_layout: Layout<'_>,
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard, clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
value: &mut Value, value: &mut Value,
size: Option<f32>, size: f32,
font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>, font: <crate::Renderer as iced_core::text::Renderer>::Font,
is_secure: bool, is_secure: bool,
on_input: Option<&dyn Fn(String) -> Message>, on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>,
@ -1083,12 +1101,18 @@ pub fn update<'a, Message>(
where where
Message: Clone, Message: Clone,
{ {
let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size().0);
let update_cache = |state, value| { let update_cache = |state, value| {
replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height); replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height);
}; };
let mut secured_value = if is_secure {
value.secure()
} else {
value.clone()
};
let unsecured_value = value;
let value = &mut secured_value;
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -1125,9 +1149,6 @@ where
// if something is already selected, we can start a drag and drop for a // if something is already selected, we can start a drag and drop for a
// single click that is on top of the selected text // single click that is on top of the selected text
// is the click on selected text? // is the click on selected text?
if is_secure {
return event::Status::Ignored;
}
if let ( if let (
Some(on_start_dnd), Some(on_start_dnd),
@ -1164,17 +1185,23 @@ where
}; };
if cursor_position.is_over(selection_bounds) { if cursor_position.is_over(selection_bounds) {
// XXX never start a dnd if the input is secure
if is_secure {
return event::Status::Ignored;
}
let text = let text =
state.selected_text(&value.to_string()).unwrap_or_default(); state.selected_text(&value.to_string()).unwrap_or_default();
state.dragging_state = state.dragging_state =
Some(DraggingState::Dnd(DndAction::empty(), text.clone())); Some(DraggingState::Dnd(DndAction::empty(), text.clone()));
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete(); editor.delete();
let message = (on_input)(editor.contents()); let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message); shell.publish(message);
shell.publish(on_start_dnd(state.clone())); shell.publish(on_start_dnd(state.clone()));
let state = state.clone(); let state_clone = state.clone();
shell.publish(on_dnd_command_produced(Box::new(move || { shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::StartDnd { platform_specific::wayland::data_device::ActionInner::StartDnd {
mime_types: SUPPORTED_TEXT_MIME_TYPES mime_types: SUPPORTED_TEXT_MIME_TYPES
@ -1185,26 +1212,18 @@ where
origin_id: window_id, origin_id: window_id,
icon_id: Some(DndIcon::Widget( icon_id: Some(DndIcon::Widget(
icon_id, icon_id,
Box::new(state.clone()), Box::new(state_clone.clone()),
)), )),
data: Box::new(TextInputString(text.clone())), data: Box::new(TextInputString(text.clone())),
} }
}))); })));
update_cache(state, &unsecured_value);
} else { } else {
update_cache(state, value);
// existing logic for setting the selection // existing logic for setting the selection
let position = if target > 0.0 { let position = if target > 0.0 {
let value = if is_secure { find_cursor_position(text_layout.bounds(), value, state, target)
value.secure()
} else {
value.clone()
};
find_cursor_position(
text_layout.bounds(),
&value,
state,
target,
)
} else { } else {
None None
}; };
@ -1219,13 +1238,8 @@ where
(None, click::Kind::Single, _) => { (None, click::Kind::Single, _) => {
// existing logic for setting the selection // existing logic for setting the selection
let position = if target > 0.0 { let position = if target > 0.0 {
let value = if is_secure { update_cache(state, value);
value.secure() find_cursor_position(text_layout.bounds(), value, state, target)
} else {
value.clone()
};
find_cursor_position(text_layout.bounds(), &value, state, target)
} else { } else {
None None
}; };
@ -1234,6 +1248,8 @@ where
state.dragging_state = Some(DraggingState::Selection); state.dragging_state = Some(DraggingState::Selection);
} }
(None | Some(DraggingState::Selection), click::Kind::Double, _) => { (None | Some(DraggingState::Selection), click::Kind::Double, _) => {
update_cache(state, value);
if is_secure { if is_secure {
state.cursor.select_all(value); state.cursor.select_all(value);
} else { } else {
@ -1249,6 +1265,7 @@ where
state.dragging_state = Some(DraggingState::Selection); state.dragging_state = Some(DraggingState::Selection);
} }
(None | Some(DraggingState::Selection), click::Kind::Triple, _) => { (None | Some(DraggingState::Selection), click::Kind::Triple, _) => {
update_cache(state, value);
state.cursor.select_all(value); state.cursor.select_all(value);
state.dragging_state = Some(DraggingState::Selection); state.dragging_state = Some(DraggingState::Selection);
} }
@ -1274,17 +1291,13 @@ where
if matches!(state.dragging_state, Some(DraggingState::Selection)) { if matches!(state.dragging_state, Some(DraggingState::Selection)) {
let target = position.x - text_layout.bounds().x; let target = position.x - text_layout.bounds().x;
let value: Value = if is_secure { update_cache(state, value);
value.secure()
} else {
value.clone()
};
let position = let position =
find_cursor_position(text_layout.bounds(), &value, state, target).unwrap_or(0); find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0);
state state
.cursor .cursor
.select_range(state.cursor.start(&value), position); .select_range(state.cursor.start(value), position);
return event::Status::Captured; return event::Status::Captured;
} }
@ -1301,16 +1314,22 @@ where
&& !state.keyboard_modifiers.command() && !state.keyboard_modifiers.command()
&& !c.is_control() && !c.is_control()
{ {
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.insert(c); editor.insert(c);
let contents = editor.contents();
let message = (on_input)(editor.contents()); let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message); shell.publish(message);
focus.updated_at = Instant::now(); focus.updated_at = Instant::now();
update_cache(state, value); let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
return event::Status::Captured; return event::Status::Captured;
} }
@ -1345,33 +1364,46 @@ where
} }
} }
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.backspace(); editor.backspace();
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(editor.contents()); let message = (on_input)(editor.contents());
shell.publish(message); shell.publish(message);
update_cache(state, value); let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
} }
keyboard::KeyCode::Delete => { keyboard::KeyCode::Delete => {
if platform::is_jump_modifier_pressed(modifiers) if platform::is_jump_modifier_pressed(modifiers)
&& state.cursor.selection(value).is_none() && state.cursor.selection(value).is_none()
{ {
if is_secure { if is_secure {
let cursor_pos = state.cursor.end(value); let cursor_pos = state.cursor.end(unsecured_value);
state.cursor.select_range(cursor_pos, value.len()); state.cursor.select_range(cursor_pos, unsecured_value.len());
} else { } else {
state.cursor.select_right_by_words(value); state.cursor.select_right_by_words(unsecured_value);
} }
} }
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete(); editor.delete();
let contents = editor.contents();
let message = (on_input)(editor.contents()); let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message); shell.publish(message);
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, value); update_cache(state, &value);
} }
keyboard::KeyCode::Left => { keyboard::KeyCode::Left => {
if platform::is_jump_modifier_pressed(modifiers) && !is_secure { if platform::is_jump_modifier_pressed(modifiers) && !is_secure {
@ -1416,22 +1448,27 @@ where
} }
} }
keyboard::KeyCode::C if state.keyboard_modifiers.command() => { keyboard::KeyCode::C if state.keyboard_modifiers.command() => {
if let Some((start, end)) = state.cursor.selection(value) { if !is_secure {
clipboard.write(value.select(start, end).to_string()); if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
}
} }
} }
// XXX if we want to allow cutting of secure text, we need to
// update the cache and decide which value to cut
keyboard::KeyCode::X if state.keyboard_modifiers.command() => { keyboard::KeyCode::X if state.keyboard_modifiers.command() => {
if let Some((start, end)) = state.cursor.selection(value) { if !is_secure {
clipboard.write(value.select(start, end).to_string()); if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
}
let mut editor = Editor::new(value, &mut state.cursor);
editor.delete();
let message = (on_input)(editor.contents());
shell.publish(message);
} }
let mut editor = Editor::new(value, &mut state.cursor);
editor.delete();
let message = (on_input)(editor.contents());
shell.publish(message);
update_cache(state, value);
} }
keyboard::KeyCode::V => { keyboard::KeyCode::V => {
if state.keyboard_modifiers.command() { if state.keyboard_modifiers.command() {
@ -1448,20 +1485,28 @@ where
Value::new(&content) Value::new(&content)
}; };
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.paste(content.clone()); editor.paste(content.clone());
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = if let Some(paste) = &on_paste { let message = if let Some(paste) = &on_paste {
(paste)(editor.contents()) (paste)(contents)
} else { } else {
(on_input)(editor.contents()) (on_input)(contents)
}; };
shell.publish(message); shell.publish(message);
state.is_pasting = Some(content); state.is_pasting = Some(content);
update_cache(state, value); let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
} else { } else {
state.is_pasting = None; state.is_pasting = None;
} }
@ -1582,13 +1627,8 @@ where
state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None); state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None);
// existing logic for setting the selection // existing logic for setting the selection
let position = if target > 0.0 { let position = if target > 0.0 {
let value = if is_secure { update_cache(state, value);
value.secure() find_cursor_position(text_layout.bounds(), value, state, target)
} else {
value.clone()
};
find_cursor_position(text_layout.bounds(), &value, state, target)
} else { } else {
None None
}; };
@ -1652,13 +1692,8 @@ where
let target = x as f32 - text_layout.bounds().x; let target = x as f32 - text_layout.bounds().x;
// existing logic for setting the selection // existing logic for setting the selection
let position = if target > 0.0 { let position = if target > 0.0 {
let value = if is_secure { update_cache(state, value);
value.secure() find_cursor_position(text_layout.bounds(), value, state, target)
} else {
value.clone()
};
find_cursor_position(text_layout.bounds(), &value, state, target)
} else { } else {
None None
}; };
@ -1728,21 +1763,26 @@ where
return event::Status::Captured; return event::Status::Captured;
}; };
let mut editor = Editor::new(value, &mut state.cursor); let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.paste(Value::new(content.as_str())); editor.paste(Value::new(content.as_str()));
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
if let Some(on_paste) = on_paste.as_ref() { if let Some(on_paste) = on_paste.as_ref() {
let message = (on_paste)(editor.contents()); let message = (on_paste)(contents);
shell.publish(message);
}
if let Some(on_paste) = on_paste {
let message = (on_paste)(editor.contents());
shell.publish(message); shell.publish(message);
} }
shell.publish(on_dnd_command_produced(Box::new(move || { shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::DndFinished platform_specific::wayland::data_device::ActionInner::DndFinished
}))); })));
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
return event::Status::Captured; return event::Status::Captured;
} }
return event::Status::Ignored; return event::Status::Ignored;
@ -2159,6 +2199,8 @@ pub struct State {
pub value: crate::Paragraph, pub value: crate::Paragraph,
pub placeholder: crate::Paragraph, pub placeholder: crate::Paragraph,
pub label: crate::Paragraph, pub label: crate::Paragraph,
pub dirty: bool,
pub is_secure: bool,
is_focused: Option<Focus>, is_focused: Option<Focus>,
dragging_state: Option<DraggingState>, dragging_state: Option<DraggingState>,
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
@ -2178,8 +2220,11 @@ struct Focus {
impl State { impl State {
/// Creates a new [`State`], representing an unfocused [`TextInput`]. /// Creates a new [`State`], representing an unfocused [`TextInput`].
pub fn new() -> Self { pub fn new(is_secure: bool) -> Self {
Self::default() Self {
is_secure,
..Self::default()
}
} }
/// Returns the current value of the selected text in the [`TextInput`]. /// Returns the current value of the selected text in the [`TextInput`].
@ -2207,8 +2252,9 @@ impl State {
} }
/// Creates a new [`State`], representing a focused [`TextInput`]. /// Creates a new [`State`], representing a focused [`TextInput`].
pub fn focused() -> Self { pub fn focused(is_secure: bool) -> Self {
Self { Self {
is_secure,
value: crate::Paragraph::new(), value: crate::Paragraph::new(),
placeholder: crate::Paragraph::new(), placeholder: crate::Paragraph::new(),
label: crate::Paragraph::new(), label: crate::Paragraph::new(),
@ -2221,6 +2267,7 @@ impl State {
last_click: None, last_click: None,
cursor: Cursor::default(), cursor: Cursor::default(),
keyboard_modifiers: keyboard::Modifiers::default(), keyboard_modifiers: keyboard::Modifiers::default(),
dirty: false,
} }
} }