Refine desktop entry field layout
This commit is contained in:
@@ -22,11 +22,11 @@ use std::{
|
||||
|
||||
use editor::{EntryEditor, FieldNavigation};
|
||||
use iced::{
|
||||
Background, Color, Element, Event, Length, Point, Rectangle, Renderer, Size, Subscription,
|
||||
Task, Theme, event, keyboard, mouse, time, touch,
|
||||
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
|
||||
Subscription, Task, Theme, event, keyboard, mouse, time, touch,
|
||||
widget::{
|
||||
button, canvas, column, container, mouse_area, pane_grid, row, scrollable, text,
|
||||
text_input, tooltip,
|
||||
text_editor, text_input, tooltip,
|
||||
},
|
||||
window,
|
||||
};
|
||||
@@ -206,7 +206,7 @@ enum Message {
|
||||
result: Result<NativeAuthenticationHandle, String>,
|
||||
},
|
||||
FieldChanged(EntryFieldId, Zeroizing<String>),
|
||||
FieldLineChanged(EntryFieldId, usize, Zeroizing<String>),
|
||||
FieldEdited(EntryFieldId, text_editor::Action),
|
||||
AddFieldLine(EntryFieldId, usize),
|
||||
AddAfter(Option<EntryFieldId>),
|
||||
Remove(EntryFieldId),
|
||||
@@ -2049,12 +2049,13 @@ impl App {
|
||||
Message::FieldChanged(id, value) => {
|
||||
self.pane_focus = PaneFocus::Content;
|
||||
self.edit(|editor| {
|
||||
editor.select(id);
|
||||
editor.replace_value(id, SecretBytes::new(value.as_bytes().to_vec()))
|
||||
});
|
||||
}
|
||||
Message::FieldLineChanged(id, line, value) => {
|
||||
Message::FieldEdited(id, action) => {
|
||||
self.pane_focus = PaneFocus::Content;
|
||||
self.edit(|editor| editor.update_value_line(id, line, value.as_bytes()));
|
||||
self.edit(|editor| editor.edit_multiline(id, action));
|
||||
}
|
||||
Message::AddFieldLine(id, line) => {
|
||||
self.pane_focus = PaneFocus::Content;
|
||||
@@ -2073,6 +2074,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::SelectField(id) => {
|
||||
self.pane_focus = PaneFocus::Content;
|
||||
if let Some(editor) = self.editor.as_mut() {
|
||||
editor.select(id);
|
||||
}
|
||||
@@ -3421,11 +3423,7 @@ fn settings_vault_id() -> iced::widget::Id {
|
||||
}
|
||||
|
||||
fn editor_field_input_id(id: EntryFieldId) -> iced::widget::Id {
|
||||
editor_field_line_input_id(id, 0)
|
||||
}
|
||||
|
||||
fn editor_field_line_input_id(id: EntryFieldId, line: usize) -> iced::widget::Id {
|
||||
format!("desktop-entry-field-{}-{line}", id.value()).into()
|
||||
format!("desktop-entry-field-{}", id.value()).into()
|
||||
}
|
||||
|
||||
fn action_hint(action: UiAction) -> String {
|
||||
@@ -3461,7 +3459,10 @@ enum Icon {
|
||||
Up,
|
||||
}
|
||||
|
||||
struct IconCanvas(Icon);
|
||||
struct IconCanvas {
|
||||
icon: Icon,
|
||||
color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
type State = ();
|
||||
@@ -3475,7 +3476,7 @@ impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
let path = canvas::Path::new(|path| match self.0 {
|
||||
let path = canvas::Path::new(|path| match self.icon {
|
||||
Icon::Add => {
|
||||
path.move_to(Point::new(8.0, 2.0));
|
||||
path.line_to(Point::new(8.0, 14.0));
|
||||
@@ -3547,7 +3548,7 @@ impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
path.line_to(Point::new(13.0, 13.0));
|
||||
path.line_to(Point::new(2.5, 13.0));
|
||||
path.line_to(Point::new(1.5, 4.0));
|
||||
if matches!(self.0, Icon::FolderAdd) {
|
||||
if matches!(self.icon, Icon::FolderAdd) {
|
||||
path.move_to(Point::new(9.0, 9.5));
|
||||
path.line_to(Point::new(13.0, 9.5));
|
||||
path.move_to(Point::new(11.0, 7.5));
|
||||
@@ -3644,7 +3645,10 @@ impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
frame.stroke(
|
||||
&path,
|
||||
canvas::Stroke::default()
|
||||
.with_color(theme.extended_palette().background.base.text)
|
||||
.with_color(
|
||||
self.color
|
||||
.unwrap_or(theme.extended_palette().background.base.text),
|
||||
)
|
||||
.with_width(1.5)
|
||||
.with_line_cap(canvas::LineCap::Round)
|
||||
.with_line_join(canvas::LineJoin::Round),
|
||||
@@ -3654,7 +3658,11 @@ impl<Message> canvas::Program<Message> for IconCanvas {
|
||||
}
|
||||
|
||||
fn icon_view(icon: Icon) -> Element<'static, Message> {
|
||||
canvas(IconCanvas(icon))
|
||||
colored_icon_view(icon, None)
|
||||
}
|
||||
|
||||
fn colored_icon_view(icon: Icon, color: Option<Color>) -> Element<'static, Message> {
|
||||
canvas(IconCanvas { icon, color })
|
||||
.width(Length::Fixed(16.0))
|
||||
.height(Length::Fixed(16.0))
|
||||
.into()
|
||||
@@ -3676,6 +3684,100 @@ fn icon_control(icon: Icon, hint: String, message: Message) -> Element<'static,
|
||||
.into()
|
||||
}
|
||||
|
||||
const ENTRY_AREA_BACKGROUND: Color = Color::from_rgb8(18, 18, 20);
|
||||
const ENTRY_FIELD_BACKGROUND: Color = Color::from_rgb8(24, 24, 27);
|
||||
const ENTRY_FIELD_ACTIVE_BACKGROUND: Color = Color::from_rgb8(47, 47, 52);
|
||||
const ENTRY_INPUT_BACKGROUND: Color = Color::from_rgb8(12, 12, 14);
|
||||
const ENTRY_TEXT: Color = Color::from_rgb8(235, 235, 238);
|
||||
const ENTRY_MUTED_TEXT: Color = Color::from_rgb8(165, 165, 172);
|
||||
const ENTRY_BORDER: Color = Color::from_rgb8(72, 72, 78);
|
||||
const ENTRY_FOCUSED_BORDER: Color = Color::from_rgb8(145, 145, 152);
|
||||
const ENTRY_LABEL_WIDTH: f32 = 128.0;
|
||||
|
||||
fn entry_icon_control(icon: Icon, hint: String, message: Message) -> Element<'static, Message> {
|
||||
tooltip(
|
||||
button(colored_icon_view(icon, Some(ENTRY_TEXT)))
|
||||
.padding(6)
|
||||
.style(entry_icon_button)
|
||||
.on_press(message),
|
||||
container(text(hint).size(12))
|
||||
.padding([5, 8])
|
||||
.style(container::dark),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.gap(4)
|
||||
.delay(Duration::from_millis(350))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn entry_icon_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: matches!(status, button::Status::Hovered | button::Status::Pressed)
|
||||
.then_some(Background::Color(ENTRY_FIELD_ACTIVE_BACKGROUND)),
|
||||
text_color: ENTRY_TEXT,
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_area_style(_theme: &Theme) -> container::Style {
|
||||
container::Style::default()
|
||||
.background(ENTRY_AREA_BACKGROUND)
|
||||
.color(ENTRY_TEXT)
|
||||
}
|
||||
|
||||
fn entry_field_style(_theme: &Theme, selected: bool) -> container::Style {
|
||||
container::Style::default()
|
||||
.background(if selected {
|
||||
ENTRY_FIELD_ACTIVE_BACKGROUND
|
||||
} else {
|
||||
ENTRY_FIELD_BACKGROUND
|
||||
})
|
||||
.color(ENTRY_TEXT)
|
||||
}
|
||||
|
||||
fn entry_value_style(_theme: &Theme) -> container::Style {
|
||||
container::Style::default()
|
||||
.background(ENTRY_INPUT_BACKGROUND)
|
||||
.color(ENTRY_TEXT)
|
||||
}
|
||||
|
||||
fn entry_input_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(ENTRY_INPUT_BACKGROUND),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
width: 1.0,
|
||||
color: if matches!(status, text_input::Status::Focused { .. }) {
|
||||
ENTRY_FOCUSED_BORDER
|
||||
} else {
|
||||
ENTRY_BORDER
|
||||
},
|
||||
},
|
||||
icon: ENTRY_MUTED_TEXT,
|
||||
placeholder: ENTRY_MUTED_TEXT,
|
||||
value: ENTRY_TEXT,
|
||||
selection: ENTRY_FIELD_ACTIVE_BACKGROUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style {
|
||||
text_editor::Style {
|
||||
background: Background::Color(ENTRY_INPUT_BACKGROUND),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
width: 1.0,
|
||||
color: if matches!(status, text_editor::Status::Focused { .. }) {
|
||||
ENTRY_FOCUSED_BORDER
|
||||
} else {
|
||||
ENTRY_BORDER
|
||||
},
|
||||
},
|
||||
placeholder: ENTRY_MUTED_TEXT,
|
||||
value: ENTRY_TEXT,
|
||||
selection: ENTRY_FIELD_ACTIVE_BACKGROUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn action_icon(icon: Icon, action: UiAction) -> Element<'static, Message> {
|
||||
icon_control(icon, action_hint(action), Message::Action(action))
|
||||
}
|
||||
@@ -4890,28 +4992,36 @@ fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message
|
||||
ViewerValue::Unavailable => "(binary value)",
|
||||
};
|
||||
let copy_hint = format!("Copy {label} (⌘C)");
|
||||
let mut field_view = column![
|
||||
row![
|
||||
button(text(label.clone()).size(12))
|
||||
let copy = entry_icon_control(
|
||||
Icon::Copy,
|
||||
copy_hint,
|
||||
Message::FieldAction(id, UiAction::CopyField),
|
||||
);
|
||||
let mut field_view = if value.contains('\n') {
|
||||
column![
|
||||
row![text(label).size(13).width(Length::Fill), copy,]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(4),
|
||||
container(text(value).size(15).wrapping(text::Wrapping::WordOrGlyph),)
|
||||
.padding(8)
|
||||
.width(Length::Fill)
|
||||
.padding([4, 6])
|
||||
.on_press(Message::SelectField(id))
|
||||
.style(if selected {
|
||||
selected_button
|
||||
} else {
|
||||
button::text
|
||||
}),
|
||||
icon_control(
|
||||
Icon::Copy,
|
||||
copy_hint,
|
||||
Message::FieldAction(id, UiAction::CopyField),
|
||||
),
|
||||
.style(entry_value_style),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(2),
|
||||
text(value).size(15),
|
||||
]
|
||||
.spacing(3);
|
||||
.spacing(4)
|
||||
} else {
|
||||
column![
|
||||
row![
|
||||
text(label).size(13).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
|
||||
text(value)
|
||||
.size(15)
|
||||
.width(Length::Fill)
|
||||
.wrapping(text::Wrapping::WordOrGlyph),
|
||||
copy,
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8),
|
||||
]
|
||||
};
|
||||
if let Some(otp) = field.metadata().otp() {
|
||||
let cadence = otp.period().map_or_else(
|
||||
|| format!("counter {}", otp.counter().unwrap_or_default()),
|
||||
@@ -4957,40 +5067,53 @@ fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message
|
||||
.size(12),
|
||||
);
|
||||
field_view = field_view.push(
|
||||
row![
|
||||
icon_control(
|
||||
Icon::Refresh,
|
||||
"Generate current OTP code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::GenerateOtp),
|
||||
),
|
||||
icon_control(
|
||||
Icon::Copy,
|
||||
"Copy current OTP code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyOtp),
|
||||
),
|
||||
icon_control(
|
||||
Icon::Link,
|
||||
"Copy OTP provisioning URI".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyOtpUri),
|
||||
),
|
||||
icon_control(
|
||||
Icon::Qr,
|
||||
"Show OTP provisioning QR code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::ShowOtpQr),
|
||||
),
|
||||
]
|
||||
.spacing(2),
|
||||
container(
|
||||
row![
|
||||
entry_icon_control(
|
||||
Icon::Refresh,
|
||||
"Generate current OTP code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::GenerateOtp),
|
||||
),
|
||||
entry_icon_control(
|
||||
Icon::Copy,
|
||||
"Copy current OTP code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyOtp),
|
||||
),
|
||||
entry_icon_control(
|
||||
Icon::Link,
|
||||
"Copy OTP provisioning URI".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyOtpUri),
|
||||
),
|
||||
entry_icon_control(
|
||||
Icon::Qr,
|
||||
"Show OTP provisioning QR code".to_owned(),
|
||||
Message::FieldAction(id, UiAction::ShowOtpQr),
|
||||
),
|
||||
]
|
||||
.spacing(2),
|
||||
)
|
||||
.align_right(Length::Fill),
|
||||
);
|
||||
}
|
||||
if let Some(diagnostic) = field.metadata().diagnostic() {
|
||||
field_view = field_view.push(text(viewer_diagnostic(diagnostic)).size(12));
|
||||
}
|
||||
rows = rows.push(container(field_view).padding([6, 4]).width(Length::Fill));
|
||||
rows = rows.push(
|
||||
mouse_area(
|
||||
container(field_view)
|
||||
.padding([7, 9])
|
||||
.width(Length::Fill)
|
||||
.style(move |theme| entry_field_style(theme, selected)),
|
||||
)
|
||||
.on_press(Message::SelectField(id)),
|
||||
);
|
||||
}
|
||||
|
||||
scrollable(rows)
|
||||
.id(viewer_scroll_id())
|
||||
container(scrollable(rows).id(viewer_scroll_id()).height(Length::Fill))
|
||||
.padding([8, 10])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(entry_area_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -5020,8 +5143,12 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
))
|
||||
.size(16)
|
||||
.width(Length::Fill),
|
||||
action_icon(Icon::Check, UiAction::Save),
|
||||
icon_control(Icon::Add, "Add field".to_owned(), Message::AddAfter(None),),
|
||||
entry_icon_control(
|
||||
Icon::Check,
|
||||
action_hint(UiAction::Save),
|
||||
Message::Action(UiAction::Save),
|
||||
),
|
||||
entry_icon_control(Icon::Add, "Add field".to_owned(), Message::AddAfter(None),),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(2)
|
||||
@@ -5041,6 +5168,7 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
|
||||
for field in editor.fields() {
|
||||
let id = field.id();
|
||||
let selected = editor.focused() == Some(id);
|
||||
let sensitive = field.metadata().sensitivity() == EntrySensitivity::Sensitive;
|
||||
let value = std::str::from_utf8(field.value()).ok();
|
||||
let label = field
|
||||
@@ -5048,65 +5176,110 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
.name()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| format!("{:?}", field.metadata().kind()));
|
||||
let input: Element<'_, Message> = if let Some(value) = value {
|
||||
let multiline = value.contains('\n');
|
||||
let mut lines = column![].spacing(2);
|
||||
for (line_index, line) in value.split('\n').enumerate() {
|
||||
let line = line.strip_suffix('\r').unwrap_or(line);
|
||||
let mut actions = row![
|
||||
entry_icon_control(Icon::Up, "Move field up".to_owned(), Message::MoveUp(id)),
|
||||
entry_icon_control(
|
||||
Icon::Down,
|
||||
"Move field down".to_owned(),
|
||||
Message::MoveDown(id)
|
||||
),
|
||||
entry_icon_control(
|
||||
Icon::Add,
|
||||
"Add field below".to_owned(),
|
||||
Message::AddAfter(Some(id)),
|
||||
),
|
||||
entry_icon_control(Icon::Delete, "Remove field".to_owned(), Message::Remove(id)),
|
||||
]
|
||||
.spacing(2);
|
||||
if sensitive && field.metadata().kind() != EntryFieldKind::OtpUri {
|
||||
actions = actions.push(entry_icon_control(
|
||||
Icon::Generate,
|
||||
"Generate password".to_owned(),
|
||||
Message::FieldAction(id, UiAction::GeneratePassword),
|
||||
));
|
||||
}
|
||||
actions = actions.push(entry_icon_control(
|
||||
Icon::Copy,
|
||||
"Copy field value (⌘C)".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyEditedField),
|
||||
));
|
||||
|
||||
let field_view = match value {
|
||||
Some(value) if value.contains('\n') => {
|
||||
let input: Element<'_, Message> = text_editor(
|
||||
editor
|
||||
.multiline_content(id)
|
||||
.expect("multiline fields have one editor content"),
|
||||
)
|
||||
.id(editor_field_input_id(id))
|
||||
.height(Length::Fixed(120.0))
|
||||
.on_action(move |action| Message::FieldEdited(id, action))
|
||||
.style(entry_editor_style)
|
||||
.into();
|
||||
column![
|
||||
row![text(label).size(14).width(Length::Fill), actions,]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(4),
|
||||
input,
|
||||
]
|
||||
.spacing(4)
|
||||
}
|
||||
Some(value) => {
|
||||
let submit = if matches!(
|
||||
field.metadata().kind(),
|
||||
EntryFieldKind::Password | EntryFieldKind::OtpUri
|
||||
) {
|
||||
Message::AddAfter(Some(id))
|
||||
} else {
|
||||
Message::AddFieldLine(id, line_index)
|
||||
Message::AddFieldLine(id, 0)
|
||||
};
|
||||
let input = text_input("Entry value", line)
|
||||
.id(editor_field_line_input_id(id, line_index))
|
||||
.on_input(move |value| {
|
||||
if multiline {
|
||||
Message::FieldLineChanged(id, line_index, Zeroizing::new(value))
|
||||
} else {
|
||||
Message::FieldChanged(id, Zeroizing::new(value))
|
||||
}
|
||||
})
|
||||
.on_submit(submit);
|
||||
lines = lines.push(input);
|
||||
let input: Element<'_, Message> = text_input("Entry value", value)
|
||||
.id(editor_field_input_id(id))
|
||||
.on_input(move |value| Message::FieldChanged(id, Zeroizing::new(value)))
|
||||
.on_submit(submit)
|
||||
.style(entry_input_style)
|
||||
.into();
|
||||
column![
|
||||
row![
|
||||
text(label).size(14).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
|
||||
input,
|
||||
actions,
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8),
|
||||
]
|
||||
}
|
||||
None => {
|
||||
let input: Element<'_, Message> = text_input("Non-UTF-8 value preserved", "")
|
||||
.style(entry_input_style)
|
||||
.into();
|
||||
column![
|
||||
row![
|
||||
text(label).size(14).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
|
||||
input,
|
||||
actions,
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8),
|
||||
]
|
||||
}
|
||||
lines.into()
|
||||
} else {
|
||||
text_input("Non-UTF-8 value preserved", "").into()
|
||||
};
|
||||
let mut actions = row![
|
||||
icon_control(Icon::Up, "Move field up".to_owned(), Message::MoveUp(id)),
|
||||
icon_control(
|
||||
Icon::Down,
|
||||
"Move field down".to_owned(),
|
||||
Message::MoveDown(id)
|
||||
),
|
||||
icon_control(
|
||||
Icon::Add,
|
||||
"Add field below".to_owned(),
|
||||
Message::AddAfter(Some(id)),
|
||||
),
|
||||
icon_control(Icon::Delete, "Remove field".to_owned(), Message::Remove(id)),
|
||||
icon_control(
|
||||
Icon::Copy,
|
||||
"Copy field value (⌘C)".to_owned(),
|
||||
Message::FieldAction(id, UiAction::CopyEditedField),
|
||||
),
|
||||
]
|
||||
.spacing(2);
|
||||
if sensitive && field.metadata().kind() != EntryFieldKind::OtpUri {
|
||||
actions = actions.push(icon_control(
|
||||
Icon::Generate,
|
||||
"Generate password".to_owned(),
|
||||
Message::FieldAction(id, UiAction::GeneratePassword),
|
||||
));
|
||||
}
|
||||
fields = fields.push(column![text(label).size(14), input, actions].spacing(4));
|
||||
fields = fields.push(
|
||||
mouse_area(
|
||||
container(field_view)
|
||||
.padding([7, 9])
|
||||
.width(Length::Fill)
|
||||
.style(move |theme| entry_field_style(theme, selected)),
|
||||
)
|
||||
.on_press(Message::SelectField(id)),
|
||||
);
|
||||
}
|
||||
scrollable(fields).height(Length::Fill).into()
|
||||
container(scrollable(fields).height(Length::Fill))
|
||||
.padding([8, 10])
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(entry_area_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn confirmation_view(action: &PendingAction) -> Element<'_, Message> {
|
||||
@@ -5857,12 +6030,32 @@ mod tests {
|
||||
assert_eq!(viewer_value(&editor.fields()[9]), ViewerValue::Unavailable);
|
||||
let comments = editor.fields()[6].id();
|
||||
editor
|
||||
.update_value_line(comments, 1, b"alpha")
|
||||
.expect("update multiline row");
|
||||
.update_raw(comments, b"comments: Recovery codes:\nalpha\ntwo")
|
||||
.expect("update multiline field");
|
||||
editor
|
||||
.add_value_line_after(comments, 2)
|
||||
.expect("append multiline row");
|
||||
assert_eq!(editor.fields()[6].value(), b"Recovery codes:\nalpha\ntwo\n");
|
||||
assert_eq!(
|
||||
editor
|
||||
.multiline_content(comments)
|
||||
.expect("multiline editor")
|
||||
.text(),
|
||||
"Recovery codes:\nalpha\ntwo\n"
|
||||
);
|
||||
editor
|
||||
.edit_multiline(
|
||||
comments,
|
||||
text_editor::Action::Edit(text_editor::Edit::Insert('!')),
|
||||
)
|
||||
.expect("edit multiline content");
|
||||
assert_eq!(
|
||||
std::str::from_utf8(editor.fields()[6].value()).expect("UTF-8 field"),
|
||||
editor
|
||||
.multiline_content(comments)
|
||||
.expect("multiline editor")
|
||||
.text()
|
||||
);
|
||||
assert!(document_has_single_totp(editor.document()));
|
||||
editor.navigate(FieldNavigation::First);
|
||||
assert_eq!(editor.focused(), Some(password));
|
||||
|
||||
Reference in New Issue
Block a user