Revamp desktop usability and multiline fields
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
//! Structured desktop editing state over storage-owned entry documents.
|
||||
|
||||
use std::{collections::BTreeSet, fmt};
|
||||
use std::fmt;
|
||||
|
||||
use ironstorage::{
|
||||
document::{
|
||||
DocumentError, EntryDocument, EntryField, EntryFieldDraft, EntryFieldId, EntrySensitivity,
|
||||
},
|
||||
document::{DocumentError, EntryDocument, EntryField, EntryFieldDraft, EntryFieldId},
|
||||
repository::SecretBytes,
|
||||
};
|
||||
|
||||
pub struct EntryEditor {
|
||||
document: EntryDocument,
|
||||
revealed: BTreeSet<EntryFieldId>,
|
||||
focused: Option<EntryFieldId>,
|
||||
dirty: bool,
|
||||
}
|
||||
@@ -29,7 +26,6 @@ impl EntryEditor {
|
||||
let focused = document.fields().first().map(EntryField::id);
|
||||
Self {
|
||||
document,
|
||||
revealed: BTreeSet::new(),
|
||||
focused,
|
||||
dirty: false,
|
||||
}
|
||||
@@ -66,10 +62,6 @@ impl EntryEditor {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
pub fn is_revealed(&self, id: EntryFieldId) -> bool {
|
||||
self.revealed.contains(&id)
|
||||
}
|
||||
|
||||
pub fn focused(&self) -> Option<EntryFieldId> {
|
||||
self.focused
|
||||
}
|
||||
@@ -108,20 +100,7 @@ impl EntryEditor {
|
||||
self.focused = Some(fields[index].id());
|
||||
}
|
||||
|
||||
pub fn toggle_reveal(&mut self, id: EntryFieldId) -> Result<(), DocumentError> {
|
||||
let field = self
|
||||
.document
|
||||
.field(id)
|
||||
.ok_or(DocumentError::UnknownField { id })?;
|
||||
if field.metadata().sensitivity() != EntrySensitivity::Sensitive {
|
||||
return Ok(());
|
||||
}
|
||||
if !self.revealed.remove(&id) {
|
||||
self.revealed.insert(id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn update_raw(&mut self, id: EntryFieldId, value: &[u8]) -> Result<(), DocumentError> {
|
||||
let unchanged = self
|
||||
.document
|
||||
@@ -131,7 +110,44 @@ impl EntryEditor {
|
||||
return Ok(());
|
||||
}
|
||||
self.document
|
||||
.update(id, EntryFieldDraft::line(value.to_vec())?)?;
|
||||
.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.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_value_line_after(
|
||||
&mut self,
|
||||
id: EntryFieldId,
|
||||
line: usize,
|
||||
) -> Result<(), DocumentError> {
|
||||
let field = self
|
||||
.document
|
||||
.field(id)
|
||||
.ok_or(DocumentError::UnknownField { id })?;
|
||||
let mut value = field.value().to_vec();
|
||||
let insertion =
|
||||
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.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -158,7 +174,6 @@ impl EntryEditor {
|
||||
.position(|field| field.id() == id)
|
||||
.ok_or(DocumentError::UnknownField { id })?;
|
||||
self.document.remove(id)?;
|
||||
self.revealed.remove(&id);
|
||||
if self.focused == Some(id) {
|
||||
self.focused = self
|
||||
.document
|
||||
@@ -209,7 +224,6 @@ impl EntryEditor {
|
||||
) -> Result<(), DocumentError> {
|
||||
self.document
|
||||
.replace_field_value(id, value.expose().to_vec())?;
|
||||
self.revealed.remove(&id);
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -219,12 +233,42 @@ impl EntryEditor {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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> {
|
||||
let mut end = 0;
|
||||
let mut lines = 0;
|
||||
for (line, segment) in value.split_inclusive(|byte| *byte == b'\n').enumerate() {
|
||||
lines = line + 1;
|
||||
end += segment.len();
|
||||
if line == target {
|
||||
return Some(end);
|
||||
}
|
||||
}
|
||||
(target == lines && (value.is_empty() || value.ends_with(b"\n"))).then_some(value.len())
|
||||
}
|
||||
|
||||
impl fmt::Debug for EntryEditor {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("EntryEditor")
|
||||
.field("document", &self.document)
|
||||
.field("revealed", &self.revealed)
|
||||
.field("focused", &self.focused)
|
||||
.field("dirty", &self.dirty)
|
||||
.finish()
|
||||
|
||||
Reference in New Issue
Block a user