Skip to content

Commit

Permalink
add zoom feat
Browse files Browse the repository at this point in the history
  • Loading branch information
SeaDve committed Dec 13, 2023
1 parent 03d2f35 commit 98f747a
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 24 deletions.
46 changes: 42 additions & 4 deletions data/graph_view/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// TODO
// - no idea why we don't recover from errors
// - fix exporting
// - animate and add more zoom controls
// - make mouse wheel zoom smooth like loupe
// - only allow resetZoom when not on original zoom level
// - improve packaging

const ZOOM_TRANSITION_DURATION_MS = 200;
const TRANSITION_DURATION_MS = 400;

const graphLoadedHandler = window.webkit.messageHandlers.graphLoaded;
const graphErrorHandler = window.webkit.messageHandlers.graphError;
const initEndHandler = window.webkit.messageHandlers.initEnd;
const graphErrorHandler = window.webkit.messageHandlers.graphError;
const graphLoadedHandler = window.webkit.messageHandlers.graphLoaded;
const zoomLevelChangedHandler = window.webkit.messageHandlers.zoomLevelChanged;

class GraphView {
constructor() {
Expand Down Expand Up @@ -57,6 +60,8 @@ class GraphView {

_handleRenderDone() {
this._svg = this._div.selectWithoutDataPropagation("svg");
this._graphviz.zoomBehavior().on("end", this._handleZoomEnd.bind(this));

this._rendering = false;

if (this._pendingUpdate) {
Expand All @@ -65,6 +70,11 @@ class GraphView {
}

graphLoadedHandler.postMessage(null);
zoomLevelChangedHandler.postMessage(this.getZoomLevel());
}

_handleZoomEnd() {
zoomLevelChangedHandler.postMessage(this.getZoomLevel());
}

_renderGraph() {
Expand Down Expand Up @@ -113,8 +123,36 @@ class GraphView {
this._renderGraph();
}

getZoomLevel() {
if (!this._svg) {
return 1;
}

return d3.zoomTransform(this._svg.node()).k;
}

setZoomScaleExtent(min, max) {
this._graphviz.zoomScaleExtent([min, max]);
}

setZoomLevelBy(factor) {
if (!this._svg) {
return;
}

this._graphviz.zoomSelection()
.transition(() => {
return d3.transition().duration(ZOOM_TRANSITION_DURATION_MS);
})
.call(this._graphviz.zoomBehavior().scaleBy, factor);
}

resetZoom() {
const transition = d3.transition().duration(TRANSITION_DURATION_MS);
if (!this._svg) {
return;
}

const transition = d3.transition().duration(ZOOM_TRANSITION_DURATION_MS);
this._graphviz.resetZoom(transition);
}

Expand Down
4 changes: 4 additions & 0 deletions data/resources/icons/scalable/actions/zoom-in-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions data/resources/icons/scalable/actions/zoom-out-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 26 additions & 7 deletions data/resources/ui/window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,34 @@
<child type="bottom">
<object class="GtkActionBar">
<child>
<object class="GtkDropDown" id="engine_drop_down"/>
</child>
<child>
<object class="GtkButton">
<property name="tooltip-text" translatable="yes">Reset Zoom</property>
<property name="icon-name">object-scale-to-fit-symbolic</property>
<property name="action-name">win.reset-graph-zoom</property>
<object class="GtkBox">
<style>
<class name="linked"/>
</style>
<child>
<object class="GtkButton">
<property name="tooltip-text" translatable="yes">Zoom Out</property>
<property name="icon-name">zoom-out-symbolic</property>
<property name="action-name">win.zoom-graph-out</property>
</object>
</child>
<child>
<object class="GtkButton" id="zoom_level_button">
<property name="action-name">win.reset-graph-zoom</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="tooltip-text" translatable="yes">Zoom In</property>
<property name="icon-name">zoom-in-symbolic</property>
<property name="action-name">win.zoom-graph-in</property>
</object>
</child>
</object>
</child>
<child type="end">
<object class="GtkDropDown" id="engine_drop_down"/>
</child>
<child type="end">
<object class="GtkRevealer" id="spinner_revealer">
<property name="can-target">False</property>
Expand Down
98 changes: 86 additions & 12 deletions src/graph_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ use crate::{config::GRAPHVIEWSRCDIR, utils};
const INIT_END_MESSAGE_ID: &str = "initEnd";
const GRAPH_ERROR_MESSAGE_ID: &str = "graphError";
const GRAPH_LOADED_MESSAGE_ID: &str = "graphLoaded";
const ZOOM_LEVEL_CHANGED_MESSAGE_ID: &str = "zoomLevelChanged";

const ZOOM_FACTOR: f64 = 1.5;
const MIN_ZOOM_LEVEL: f64 = 0.05;
const MAX_ZOOM_LEVEL: f64 = 20.0;

#[derive(Debug, Clone, Copy, glib::Variant, glib::Enum)]
#[repr(i32)]
Expand Down Expand Up @@ -66,6 +71,12 @@ mod imp {
pub struct GraphView {
#[property(get)]
pub(super) is_graph_loaded: Cell<bool>,
#[property(get)]
pub(super) zoom_level: Cell<f64>,
#[property(get)]
pub(super) can_zoom_in: Cell<bool>,
#[property(get)]
pub(super) can_zoom_out: Cell<bool>,

pub(super) view: webkit::WebView,
pub(super) index_loaded: OnceCell<()>,
Expand All @@ -91,6 +102,9 @@ mod imp {
.property("settings", settings)
.property("web-context", context)
.build(),
zoom_level: Cell::new(1.0),
can_zoom_in: Cell::new(false),
can_zoom_out: Cell::new(false),
index_loaded: OnceCell::new(),
}
}
Expand Down Expand Up @@ -146,6 +160,13 @@ mod imp {
obj.set_graph_loaded(true);
}),
);
obj.connect_script_message_received(
ZOOM_LEVEL_CHANGED_MESSAGE_ID,
clone!(@weak obj => move |_, value| {
let zoom_level = value.to_double();
obj.set_zoom_level(zoom_level);
}),
);

utils::spawn(
glib::Priority::default(),
Expand Down Expand Up @@ -203,6 +224,16 @@ impl GraphView {
Ok(())
}

pub async fn zoom_in(&self) -> Result<()> {
self.set_zoom_level_by(ZOOM_FACTOR).await?;
Ok(())
}

pub async fn zoom_out(&self) -> Result<()> {
self.set_zoom_level_by(ZOOM_FACTOR.recip()).await?;
Ok(())
}

pub async fn reset_zoom(&self) -> Result<()> {
self.call_js_func("graphView.resetZoom", &[]).await?;
Ok(())
Expand All @@ -221,10 +252,19 @@ impl GraphView {
Ok(Some(bytes))
}

async fn call_js_func(&self, func_name: &str, args: &[&dyn ToVariant]) -> Result<Value> {
let imp = self.imp();
async fn set_zoom_level_by(&self, factor: f64) -> Result<()> {
self.call_js_func("graphView.setZoomLevelBy", &[&factor])
.await?;
Ok(())
}

async fn call_js_func(&self, func_name: &str, args: &[&dyn ToVariant]) -> Result<Value> {
self.ensure_view_initialized().await?;
self.call_js_func_inner(func_name, args).await
}

async fn call_js_func_inner(&self, func_name: &str, args: &[&dyn ToVariant]) -> Result<Value> {
let imp = self.imp();

let args = args
.iter()
Expand Down Expand Up @@ -281,9 +321,20 @@ impl GraphView {
}

self.imp().is_graph_loaded.set(is_graph_loaded);
self.update_can_zoom();
self.notify_is_graph_loaded();
}

fn set_zoom_level(&self, zoom_level: f64) {
if zoom_level == self.zoom_level() {
return;
}

self.imp().zoom_level.set(zoom_level);
self.update_can_zoom();
self.notify_zoom_level();
}

async fn ensure_view_initialized(&self) -> Result<()> {
let imp = self.imp();

Expand Down Expand Up @@ -333,26 +384,49 @@ impl GraphView {
user_content_manager.unregister_script_message_handler(INIT_END_MESSAGE_ID, None);
user_content_manager.disconnect(init_handler_id);

let ret = imp
.view
.call_async_javascript_function_future(
"return graphView.graphvizVersion()",
None,
None,
None,
)
self.call_js_func_inner(
"graphView.setZoomScaleExtent",
&[&MIN_ZOOM_LEVEL, &MAX_ZOOM_LEVEL],
)
.await
.context("Failed to set zoom scale extent")?;

let version = self
.call_js_func_inner("graphView.graphvizVersion", &[])
.await
.context("Failed to get version")?;
let version = ret.to_str();
.context("Failed to get version")?
.to_str();

tracing::debug!(%version, "Initialized Graphviz");

let zoom_level = self
.call_js_func_inner("graphView.getZoomLevel", &[])
.await
.context("Failed to get zoom level")?
.to_double();
self.set_zoom_level(zoom_level);

anyhow::Ok(())
})
.await?;

Ok(())
}

fn update_can_zoom(&self) {
let imp = self.imp();

let is_graph_loaded = self.is_graph_loaded();
let zoom_level = self.zoom_level();

imp.can_zoom_in
.set(zoom_level < MAX_ZOOM_LEVEL && is_graph_loaded);
imp.can_zoom_out
.set(zoom_level > MIN_ZOOM_LEVEL && is_graph_loaded);

self.notify_can_zoom_in();
self.notify_can_zoom_out();
}
}

impl Default for GraphView {
Expand Down
52 changes: 51 additions & 1 deletion src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use crate::{

// TODO
// * Find and replace
// * Better viewer, with bird's eye view
// * Bird's eye view of graph
// * Full screen view of graph
// * Tabs and/or multiple windows
// * Recent files
// * dot language server, with error handling on text view
Expand Down Expand Up @@ -105,6 +106,8 @@ mod imp {
#[template_child]
pub(super) engine_drop_down: TemplateChild<gtk::DropDown>,
#[template_child]
pub(super) zoom_level_button: TemplateChild<gtk::Button>,
#[template_child]
pub(super) spinner_revealer: TemplateChild<gtk::Revealer>,

pub(super) document_binding_group: glib::BindingGroup,
Expand Down Expand Up @@ -194,6 +197,18 @@ mod imp {
}
});

klass.install_action_async("win.zoom-graph-in", None, |obj, _, _| async move {
if let Err(err) = obj.imp().graph_view.zoom_in().await {
tracing::error!("Failed to zoom in: {:?}", err);
}
});

klass.install_action_async("win.zoom-graph-out", None, |obj, _, _| async move {
if let Err(err) = obj.imp().graph_view.zoom_out().await {
tracing::error!("Failed to zoom out: {:?}", err);
}
});

klass.install_action_async("win.reset-graph-zoom", None, |obj, _, _| async move {
if let Err(err) = obj.imp().graph_view.reset_zoom().await {
tracing::error!("Failed to reset zoom: {:?}", err);
Expand Down Expand Up @@ -309,6 +324,18 @@ mod imp {
obj.update_export_graph_action();
tracing::error!("Failed to draw graph: {}", message);
}));
self.graph_view
.connect_can_zoom_in_notify(clone!(@weak obj => move |_| {
obj.update_zoom_in_action();
}));
self.graph_view
.connect_can_zoom_out_notify(clone!(@weak obj => move |_| {
obj.update_zoom_out_action();
}));
self.graph_view
.connect_zoom_level_notify(clone!(@weak obj => move |_| {
obj.update_zoom_level_button();
}));

utils::spawn(
glib::Priority::DEFAULT_IDLE,
Expand All @@ -319,6 +346,9 @@ mod imp {

obj.set_document(&Document::draft());
obj.update_export_graph_action();
obj.update_zoom_in_action();
obj.update_zoom_out_action();
obj.update_zoom_level_button();

obj.load_window_state();
}
Expand Down Expand Up @@ -719,6 +749,26 @@ impl Window {
!imp.spinner_revealer.reveals_child() && imp.graph_view.is_graph_loaded(),
);
}

fn update_zoom_in_action(&self) {
let imp = self.imp();

self.action_set_enabled("win.zoom-graph-in", imp.graph_view.can_zoom_in());
}

fn update_zoom_out_action(&self) {
let imp = self.imp();

self.action_set_enabled("win.zoom-graph-out", imp.graph_view.can_zoom_out());
}

fn update_zoom_level_button(&self) {
let imp = self.imp();

let zoom_level = imp.graph_view.zoom_level();
imp.zoom_level_button
.set_label(&format!("{:.0}%", zoom_level * 100.0));
}
}

fn graphviz_file_filters() -> gio::ListStore {
Expand Down

0 comments on commit 98f747a

Please sign in to comment.