This commit is contained in:
Alex Wied
2026-07-14 21:31:15 -04:00
parent fc970084d7
commit 08b7f30cdf
7 changed files with 227 additions and 15 deletions
+44 -12
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::rdf::ontology::{IndexEntry, LabeledIri, Ontology}; use crate::rdf::ontology::{IndexEntry, LabeledIri, Ontology};
use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use crate::rdf::term_helper::{TermHelper, TermHelperMut};
use gl_search::{Schema, SearchDocument, SearchIndex, doc, IndexWriter, Value}; use gl_search::{Schema, SearchDocument, SearchIndex, doc, Value};
use http::StatusCode; use http::StatusCode;
use iced::alignment::Horizontal; use iced::alignment::Horizontal;
use iced::widget::button::Style; use iced::widget::button::Style;
@@ -21,10 +21,12 @@ use oxigraph::model::vocab::{rdf, rdfs};
use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term}; use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term};
use tracing::{debug, debug_span, error, trace}; use tracing::{debug, debug_span, error, trace};
use tracing::span::EnteredSpan; use tracing::span::EnteredSpan;
use crate::navigator::Navigator;
use crate::rdf::curie::CurieHelper; use crate::rdf::curie::CurieHelper;
use crate::rdf::language; use crate::rdf::language;
use crate::rdf::vocab::rda; use crate::rdf::vocab::rda;
use crate::widget::iri_input::iri_input; use crate::widget::iri_input::iri_input;
use crate::widget::navigation_area::navigation_area;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum Message { pub(crate) enum Message {
@@ -37,6 +39,7 @@ pub(crate) enum Message {
WindowClosed(window::Id), WindowClosed(window::Id),
URLInputChanged(String), URLInputChanged(String),
URLInputSubmitted, URLInputSubmitted,
NavigateTo(Url),
FetchDocument(Url), FetchDocument(Url),
LoadDocument(RdfSource<KeyedDataset<RowState>>), LoadDocument(RdfSource<KeyedDataset<RowState>>),
ShowNewDocumentButtons, ShowNewDocumentButtons,
@@ -64,6 +67,8 @@ pub(crate) enum Message {
NewDocument(NamedNode), NewDocument(NamedNode),
NavigateToPredicate(QuadKey), NavigateToPredicate(QuadKey),
NavigateToObject(QuadKey), NavigateToObject(QuadKey),
NavigateBack,
NavigateForward,
ResetState, ResetState,
} }
@@ -95,6 +100,7 @@ pub(crate) struct Publisher {
abbreviated_datatypes: Vec<String>, abbreviated_datatypes: Vec<String>,
window_id: window::Id, window_id: window::Id,
url_input: String, url_input: String,
navigator: Navigator<Url>,
document: RdfSource<KeyedDataset<RowState>>, document: RdfSource<KeyedDataset<RowState>>,
hovered_row: Option<QuadKey>, hovered_row: Option<QuadKey>,
search_state: Option<SearchState>, search_state: Option<SearchState>,
@@ -139,8 +145,9 @@ impl Publisher {
)) ))
.build(); .build();
let url_input = "http://fedora.quill.lan/rest/".to_string(); let starting_url = Url::parse("http://fedora.quill.lan/rest/").unwrap();
let document = RdfSource::new(Url::parse(&url_input).unwrap()); let navigator = Navigator::new(starting_url.clone());
let document = RdfSource::new(starting_url.clone());
( (
Self { Self {
@@ -149,7 +156,8 @@ impl Publisher {
ontology, ontology,
abbreviated_datatypes, abbreviated_datatypes,
window_id: id, window_id: id,
url_input, url_input: starting_url.to_string(),
navigator,
document, document,
hovered_row: None, hovered_row: None,
search_state: None, search_state: None,
@@ -255,6 +263,10 @@ impl Publisher {
} }
Message::URLInputSubmitted => { Message::URLInputSubmitted => {
let url = Url::parse(&self.url_input).expect("Invalid URL"); let url = Url::parse(&self.url_input).expect("Invalid URL");
self.navigator.goto(url.clone());
task = Task::done(Message::NavigateTo(url));
}
Message::NavigateTo(url) => {
task = Task::done(Message::DeleteAllRows) task = Task::done(Message::DeleteAllRows)
.chain(Task::done(Message::ResetState)) .chain(Task::done(Message::ResetState))
.chain(Task::done(Message::FetchDocument(url))); .chain(Task::done(Message::FetchDocument(url)));
@@ -558,18 +570,30 @@ impl Publisher {
} }
Message::NavigateToPredicate(key) => { Message::NavigateToPredicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
self.url_input = quad.predicate.as_str().to_string(); let url = Url::parse(quad.predicate.as_str()).expect("Invalid URL");
task = Task::done(Message::URLInputSubmitted); task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
} }
} }
Message::NavigateToObject(key) => { Message::NavigateToObject(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
if let Term::NamedNode(node) = &quad.object { if let Term::NamedNode(node) = &quad.object {
self.url_input = node.as_str().to_string(); let url = Url::parse(node.as_str()).expect("Invalid URL");
task = Task::done(Message::URLInputSubmitted); task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
} }
} }
} }
Message::NavigateBack => {
let url = self.navigator.back().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
Message::NavigateForward => {
let url = self.navigator.forward().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
_ => {} _ => {}
} }
task task
@@ -634,14 +658,14 @@ impl Publisher {
}) })
.unwrap_or(Horizontal::Left); .unwrap_or(Horizontal::Left);
let object_input_base = iri_input(&self.curie_helper, "Object", value.as_str()); let object_input_base = iri_input(&self.curie_helper, "Object", value.as_str())
.on_shift_click(Message::NavigateToObject(key));
let object_input = if self.ontology.is_read_only(triple.as_ref()) { let object_input = if self.ontology.is_read_only(triple.as_ref()) {
object_input_base object_input_base
} else { } else {
object_input_base.align_x(value_alignment) object_input_base.align_x(value_alignment)
.on_input(move |value| Message::ObjectUpdated(key, value)) .on_input(move |value| Message::ObjectUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key))) .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key)))
.on_shift_click(Message::NavigateToObject(key))
}; };
let selected_datatype = term.datatype() let selected_datatype = term.datatype()
@@ -797,6 +821,10 @@ impl Publisher {
].into(); ].into();
} }
let back_button = button("\u{21e6}").on_press(Message::NavigateBack);
let forward_button = button("\u{21e8}").on_press(Message::NavigateForward);
let add_row_button = button("Add row").on_press(Message::AddRow(None)); let add_row_button = button("Add row").on_press(Message::AddRow(None));
let reindex_ontology_button = button("Rebuild index").on_press(Message::RebuildIndex); let reindex_ontology_button = button("Rebuild index").on_press(Message::RebuildIndex);
@@ -831,8 +859,10 @@ impl Publisher {
save_button_base save_button_base
}; };
let content = column![ let content = navigation_area(column![
row![ row![
back_button,
forward_button,
add_row_button, add_row_button,
reindex_ontology_button, reindex_ontology_button,
traverse_button, traverse_button,
@@ -841,7 +871,9 @@ impl Publisher {
], ],
scrollable(body), scrollable(body),
space::vertical(), space::vertical(),
]; ])
.on_back_click(Message::NavigateBack)
.on_forward_click(Message::NavigateForward);
if self.show_overwrite_confirmation { if self.show_overwrite_confirmation {
let confirmation_modal = container(column![ let confirmation_modal = container(column![
+1
View File
@@ -3,6 +3,7 @@ mod error;
mod rdf; mod rdf;
mod windows; mod windows;
mod widget; mod widget;
mod navigator;
use crate::app::Publisher; use crate::app::Publisher;
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
+36
View File
@@ -0,0 +1,36 @@
pub struct Navigator<T> {
entries: Vec<T>,
active_entry: usize,
}
impl<T> Navigator<T> {
pub fn new(start: T) -> Self {
Self {
entries: vec![start],
active_entry: 0,
}
}
pub fn goto(&mut self, target: T) {
self.entries.push(target);
self.active_entry += 1;
}
pub fn current(&self) -> &T {
&self.entries[self.active_entry]
}
pub fn back(&mut self) -> &T {
if self.active_entry > 0 {
self.active_entry -= 1;
}
self.current()
}
pub fn forward(&mut self) -> &T {
if self.active_entry < self.entries.len() - 1 {
self.active_entry += 1;
}
self.current()
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::error; use crate::error;
use crate::rdf::vocab::gl; use crate::rdf::vocab::gl;
use oxigraph::model::vocab::{rdf, rdfs, xsd}; use oxigraph::model::vocab::{rdf, rdfs, xsd};
use oxigraph::model::{Dataset, Graph, GraphName, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef}; use oxigraph::model::{Dataset, Graph, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
use oxigraph::sparql::{PreparedSparqlQuery, QueryResults, SparqlEvaluator}; use oxigraph::sparql::{PreparedSparqlQuery, QueryResults, SparqlEvaluator};
use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::Display; use std::fmt::Display;
+6 -1
View File
@@ -1,4 +1,3 @@
use std::collections::BTreeMap;
use iced::{alignment, keyboard, widget, Element, Event, Length, Rectangle, Size}; use iced::{alignment, keyboard, widget, Element, Event, Length, Rectangle, Size};
use iced::advanced::{mouse, text, Shell}; use iced::advanced::{mouse, text, Shell};
use iced::advanced::layout::{Limits, Node}; use iced::advanced::layout::{Limits, Node};
@@ -7,6 +6,7 @@ use iced::advanced::{Layout, Widget};
use iced::advanced::widget::{tree, Tree}; use iced::advanced::widget::{tree, Tree};
use iced::mouse::{Cursor, Interaction}; use iced::mouse::{Cursor, Interaction};
use iced::widget::text_input::Catalog; use iced::widget::text_input::Catalog;
use tracing::debug;
use crate::rdf::curie::CurieHelper; use crate::rdf::curie::CurieHelper;
pub struct State { pub struct State {
@@ -42,21 +42,25 @@ where
} }
} }
#[must_use]
pub fn align_x(mut self, alignment: impl Into<alignment::Horizontal>) -> Self { pub fn align_x(mut self, alignment: impl Into<alignment::Horizontal>) -> Self {
self.text_input = self.text_input.align_x(alignment); self.text_input = self.text_input.align_x(alignment);
self self
} }
#[must_use]
pub fn on_control_click(mut self, on_control_click: Message) -> Self { pub fn on_control_click(mut self, on_control_click: Message) -> Self {
self.on_control_click = Some(on_control_click); self.on_control_click = Some(on_control_click);
self self
} }
#[must_use]
pub fn on_shift_click(mut self, on_shift_click: Message) -> Self { pub fn on_shift_click(mut self, on_shift_click: Message) -> Self {
self.on_shift_click = Some(on_shift_click); self.on_shift_click = Some(on_shift_click);
self self
} }
#[must_use]
pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self { pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
let wrapped = move |value: String| { let wrapped = move |value: String| {
let expanded_value = self.curie_helper.expand(&value); let expanded_value = self.curie_helper.expand(&value);
@@ -67,6 +71,7 @@ where
self self
} }
#[must_use]
pub fn on_submit(mut self, message: Message) -> Self { pub fn on_submit(mut self, message: Message) -> Self {
self.text_input = self.text_input.on_submit(message); self.text_input = self.text_input.on_submit(message);
self self
+1
View File
@@ -1 +1,2 @@
pub mod iri_input; pub mod iri_input;
pub mod navigation_area;
+137
View File
@@ -0,0 +1,137 @@
use iced::advanced::{mouse, text, Layout, Shell, Widget};
use iced::{Element, Event, Length, Rectangle, Size};
use iced::advanced::layout::{Limits, Node};
use iced::advanced::renderer::Style;
use iced::advanced::widget::{Operation, Tree};
use iced::mouse::Cursor;
use iced::widget::text_input::Catalog;
pub struct NavigationArea<'a, Message, Theme, Renderer>
where
Theme: Catalog,
Renderer: text::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
on_back_click: Option<Message>,
on_forward_click: Option<Message>,
}
impl<'a, Message, Theme, Renderer> NavigationArea<'a, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
#[must_use]
pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
Self {
content: content.into(),
on_back_click: None,
on_forward_click: None,
}
}
#[must_use]
pub fn on_back_click(mut self, message: Message) -> Self {
self.on_back_click = Some(message);
self
}
#[must_use]
pub fn on_forward_click(mut self, message: Message) -> Self {
self.on_forward_click = Some(message);
self
}
}
impl<'a, Message, Theme, Renderer> From<NavigationArea<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: Catalog + 'a,
Renderer: text::Renderer + 'a,
{
fn from(value: NavigationArea<'a, Message, Theme, Renderer>) -> Self {
Element::new(value)
}
}
pub fn navigation_area<'a, Message, Theme, Renderer>(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> NavigationArea<'a, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
NavigationArea::new(content)
}
impl <Message, Theme, Renderer> Widget<Message, Theme, Renderer> for NavigationArea<'_, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
Renderer: text::Renderer,
{
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn draw(&self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &Style, layout: Layout<'_>, cursor: Cursor, viewport: &Rectangle) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.content));
}
fn operate(&mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(&mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: Cursor, renderer: &Renderer, shell: &mut Shell<'_, Message>, viewport: &Rectangle) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
shell,
viewport,
);
if shell.is_event_captured() {
return;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Back)) => {
if let Some(on_back_click) = &self.on_back_click {
shell.publish(on_back_click.clone());
shell.capture_event();
}
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Forward)) => {
if let Some(on_forward_click) = &self.on_forward_click {
shell.publish(on_forward_click.clone());
shell.capture_event();
}
}
_ => {}
}
}
}