Refine desktop entry field layout

This commit is contained in:
2026-08-11 06:26:59 +02:00
parent 4163ec5e25
commit 2a8c941a65
2 changed files with 379 additions and 149 deletions

View File

@@ -2,6 +2,7 @@
use std::fmt;
use iced::widget::text_editor;
use ironstorage::{
document::{DocumentError, EntryDocument, EntryField, EntryFieldDraft, EntryFieldId},
repository::SecretBytes,
@@ -10,6 +11,7 @@ use ironstorage::{
pub struct EntryEditor {
document: EntryDocument,
focused: Option<EntryFieldId>,
multiline: Vec<(EntryFieldId, text_editor::Content)>,
dirty: bool,
}
@@ -24,9 +26,20 @@ pub enum FieldNavigation {
impl EntryEditor {
pub fn new(document: EntryDocument) -> Self {
let focused = document.fields().first().map(EntryField::id);
let multiline = document
.fields()
.iter()
.filter_map(|field| {
let value = std::str::from_utf8(field.value()).ok()?;
value
.contains('\n')
.then(|| (field.id(), text_editor::Content::with_text(value)))
})
.collect();
Self {
document,
focused,
multiline,
dirty: false,
}
}
@@ -66,6 +79,12 @@ impl EntryEditor {
self.focused
}
pub fn multiline_content(&self, id: EntryFieldId) -> Option<&text_editor::Content> {
self.multiline
.iter()
.find_map(|(field_id, content)| (*field_id == id).then_some(content))
}
pub fn focused_ratio(&self) -> f32 {
let fields = self.document.fields();
self.focused
@@ -81,6 +100,33 @@ impl EntryEditor {
}
}
pub fn edit_multiline(
&mut self,
id: EntryFieldId,
action: text_editor::Action,
) -> Result<(), DocumentError> {
let previous = self
.document
.field(id)
.ok_or(DocumentError::UnknownField { id })?
.value();
let value = {
let content = self
.multiline
.iter_mut()
.find_map(|(field_id, content)| (*field_id == id).then_some(content))
.ok_or(DocumentError::UnknownField { id })?;
content.perform(action);
content.text().into_bytes()
};
self.focused = Some(id);
if value != previous {
self.document.replace_field_value(id, value)?;
self.dirty = true;
}
Ok(())
}
pub fn navigate(&mut self, navigation: FieldNavigation) {
let fields = self.document.fields();
if fields.is_empty() {
@@ -111,25 +157,7 @@ impl EntryEditor {
}
self.document
.update(id, EntryFieldDraft::multiline(value.to_vec()))?;
self.dirty = true;
Ok(())
}
pub fn update_value_line(
&mut self,
id: EntryFieldId,
line: usize,
replacement: &[u8],
) -> Result<(), DocumentError> {
let field = self
.document
.field(id)
.ok_or(DocumentError::UnknownField { id })?;
let mut value = field.value().to_vec();
let range =
value_line_range(&value, line).ok_or(DocumentError::InvalidIndex { index: line })?;
value.splice(range, replacement.iter().copied());
self.document.replace_field_value(id, value)?;
self.sync_multiline(id);
self.dirty = true;
Ok(())
}
@@ -148,6 +176,7 @@ impl EntryEditor {
value_line_end(&value, line).ok_or(DocumentError::InvalidIndex { index: line })?;
value.splice(insertion..insertion, *b"\n");
self.document.replace_field_value(id, value)?;
self.sync_multiline(id);
self.dirty = true;
Ok(())
}
@@ -174,6 +203,7 @@ impl EntryEditor {
.position(|field| field.id() == id)
.ok_or(DocumentError::UnknownField { id })?;
self.document.remove(id)?;
self.multiline.retain(|(field_id, _)| *field_id != id);
if self.focused == Some(id) {
self.focused = self
.document
@@ -224,6 +254,7 @@ impl EntryEditor {
) -> Result<(), DocumentError> {
self.document
.replace_field_value(id, value.expose().to_vec())?;
self.sync_multiline(id);
self.dirty = true;
Ok(())
}
@@ -231,24 +262,30 @@ impl EntryEditor {
pub fn copy_value(&self, id: EntryFieldId) -> Result<SecretBytes, DocumentError> {
self.document.copy_field_value(id)
}
}
fn value_line_range(value: &[u8], target: usize) -> Option<std::ops::Range<usize>> {
let mut start = 0;
let mut lines = 0;
for (line, segment) in value.split_inclusive(|byte| *byte == b'\n').enumerate() {
lines = line + 1;
let mut end = start + segment.len() - usize::from(segment.ends_with(b"\n"));
if end > start && value[end - 1] == b'\r' {
end -= 1;
fn sync_multiline(&mut self, id: EntryFieldId) {
let value = self
.document
.field(id)
.and_then(|field| std::str::from_utf8(field.value()).ok())
.filter(|value| value.contains('\n'));
let existing = self
.multiline
.iter()
.position(|(field_id, _)| *field_id == id);
match (existing, value) {
(Some(index), Some(value)) => {
self.multiline[index].1 = text_editor::Content::with_text(value);
}
(None, Some(value)) => self
.multiline
.push((id, text_editor::Content::with_text(value))),
(Some(index), None) => {
self.multiline.remove(index);
}
(None, None) => {}
}
if line == target {
return Some(start..end);
}
start += segment.len();
}
(target == lines && (value.is_empty() || value.ends_with(b"\n")))
.then_some(value.len()..value.len())
}
fn value_line_end(value: &[u8], target: usize) -> Option<usize> {