diff --git a/publish/src/app.rs b/publish/src/app.rs index f14aa3e..24d2705 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::rdf::ontology::{IndexEntry, LabeledIri, Ontology}; 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 iced::alignment::Horizontal; use iced::widget::button::Style; @@ -21,10 +21,12 @@ use oxigraph::model::vocab::{rdf, rdfs}; use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term}; use tracing::{debug, debug_span, error, trace}; use tracing::span::EnteredSpan; +use crate::navigator::Navigator; use crate::rdf::curie::CurieHelper; use crate::rdf::language; use crate::rdf::vocab::rda; use crate::widget::iri_input::iri_input; +use crate::widget::navigation_area::navigation_area; #[derive(Clone, Debug)] pub(crate) enum Message { @@ -37,6 +39,7 @@ pub(crate) enum Message { WindowClosed(window::Id), URLInputChanged(String), URLInputSubmitted, + NavigateTo(Url), FetchDocument(Url), LoadDocument(RdfSource>), ShowNewDocumentButtons, @@ -64,6 +67,8 @@ pub(crate) enum Message { NewDocument(NamedNode), NavigateToPredicate(QuadKey), NavigateToObject(QuadKey), + NavigateBack, + NavigateForward, ResetState, } @@ -95,6 +100,7 @@ pub(crate) struct Publisher { abbreviated_datatypes: Vec, window_id: window::Id, url_input: String, + navigator: Navigator, document: RdfSource>, hovered_row: Option, search_state: Option, @@ -139,8 +145,9 @@ impl Publisher { )) .build(); - let url_input = "http://fedora.quill.lan/rest/".to_string(); - let document = RdfSource::new(Url::parse(&url_input).unwrap()); + let starting_url = Url::parse("http://fedora.quill.lan/rest/").unwrap(); + let navigator = Navigator::new(starting_url.clone()); + let document = RdfSource::new(starting_url.clone()); ( Self { @@ -149,7 +156,8 @@ impl Publisher { ontology, abbreviated_datatypes, window_id: id, - url_input, + url_input: starting_url.to_string(), + navigator, document, hovered_row: None, search_state: None, @@ -255,6 +263,10 @@ impl Publisher { } Message::URLInputSubmitted => { 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) .chain(Task::done(Message::ResetState)) .chain(Task::done(Message::FetchDocument(url))); @@ -558,18 +570,30 @@ impl Publisher { } Message::NavigateToPredicate(key) => { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { - self.url_input = quad.predicate.as_str().to_string(); - task = Task::done(Message::URLInputSubmitted); + let url = Url::parse(quad.predicate.as_str()).expect("Invalid URL"); + task = Task::done(Message::URLInputChanged(url.to_string())) + .chain(Task::done(Message::URLInputSubmitted)); } } Message::NavigateToObject(key) => { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { if let Term::NamedNode(node) = &quad.object { - self.url_input = node.as_str().to_string(); - task = Task::done(Message::URLInputSubmitted); + let url = Url::parse(node.as_str()).expect("Invalid URL"); + 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 @@ -634,14 +658,14 @@ impl Publisher { }) .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()) { object_input_base } else { object_input_base.align_x(value_alignment) .on_input(move |value| Message::ObjectUpdated(key, value)) .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key))) - .on_shift_click(Message::NavigateToObject(key)) }; let selected_datatype = term.datatype() @@ -797,6 +821,10 @@ impl Publisher { ].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 reindex_ontology_button = button("Rebuild index").on_press(Message::RebuildIndex); @@ -831,8 +859,10 @@ impl Publisher { save_button_base }; - let content = column![ + let content = navigation_area(column![ row![ + back_button, + forward_button, add_row_button, reindex_ontology_button, traverse_button, @@ -841,7 +871,9 @@ impl Publisher { ], scrollable(body), space::vertical(), - ]; + ]) + .on_back_click(Message::NavigateBack) + .on_forward_click(Message::NavigateForward); if self.show_overwrite_confirmation { let confirmation_modal = container(column![ diff --git a/publish/src/main.rs b/publish/src/main.rs index e444e6d..1c96292 100644 --- a/publish/src/main.rs +++ b/publish/src/main.rs @@ -3,6 +3,7 @@ mod error; mod rdf; mod windows; mod widget; +mod navigator; use crate::app::Publisher; use tracing_subscriber::layer::SubscriberExt; diff --git a/publish/src/navigator.rs b/publish/src/navigator.rs new file mode 100644 index 0000000..b6af0a7 --- /dev/null +++ b/publish/src/navigator.rs @@ -0,0 +1,36 @@ +pub struct Navigator { + entries: Vec, + active_entry: usize, +} + +impl Navigator { + 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() + } +} \ No newline at end of file diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index 9b13e05..7ab21ef 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -1,7 +1,7 @@ use crate::error; use crate::rdf::vocab::gl; 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 std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Display; diff --git a/publish/src/widget/iri_input.rs b/publish/src/widget/iri_input.rs index d3ae003..6621203 100644 --- a/publish/src/widget/iri_input.rs +++ b/publish/src/widget/iri_input.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use iced::{alignment, keyboard, widget, Element, Event, Length, Rectangle, Size}; use iced::advanced::{mouse, text, Shell}; use iced::advanced::layout::{Limits, Node}; @@ -7,6 +6,7 @@ use iced::advanced::{Layout, Widget}; use iced::advanced::widget::{tree, Tree}; use iced::mouse::{Cursor, Interaction}; use iced::widget::text_input::Catalog; +use tracing::debug; use crate::rdf::curie::CurieHelper; pub struct State { @@ -42,21 +42,25 @@ where } } + #[must_use] pub fn align_x(mut self, alignment: impl Into) -> Self { self.text_input = self.text_input.align_x(alignment); self } + #[must_use] pub fn on_control_click(mut self, on_control_click: Message) -> Self { self.on_control_click = Some(on_control_click); self } + #[must_use] pub fn on_shift_click(mut self, on_shift_click: Message) -> Self { self.on_shift_click = Some(on_shift_click); self } + #[must_use] pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self { let wrapped = move |value: String| { let expanded_value = self.curie_helper.expand(&value); @@ -67,6 +71,7 @@ where self } + #[must_use] pub fn on_submit(mut self, message: Message) -> Self { self.text_input = self.text_input.on_submit(message); self diff --git a/publish/src/widget/mod.rs b/publish/src/widget/mod.rs index 2a82a1e..55056c9 100644 --- a/publish/src/widget/mod.rs +++ b/publish/src/widget/mod.rs @@ -1 +1,2 @@ -pub mod iri_input; \ No newline at end of file +pub mod iri_input; +pub mod navigation_area; \ No newline at end of file diff --git a/publish/src/widget/navigation_area.rs b/publish/src/widget/navigation_area.rs new file mode 100644 index 0000000..9ee56f0 --- /dev/null +++ b/publish/src/widget/navigation_area.rs @@ -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, + on_forward_click: Option, +} + +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>) -> 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> +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>) -> NavigationArea<'a, Message, Theme, Renderer> +where + Message: Clone, + Theme: Catalog, + Renderer: text::Renderer, +{ + NavigationArea::new(content) +} + +impl Widget for NavigationArea<'_, Message, Theme, Renderer> +where + Message: Clone, + Theme: Catalog, + Renderer: text::Renderer, +{ + fn size(&self) -> Size { + 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(); + } + } + _ => {} + } + } +} \ No newline at end of file