Skip to content

Commit

Permalink
Support re-ordering VC tabs by DnD
Browse files Browse the repository at this point in the history
  • Loading branch information
Ameobea committed Jul 5, 2024
1 parent 6413786 commit 7172333
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 69 deletions.
3 changes: 3 additions & 0 deletions engine/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ pub fn delete_vc_by_id(id: &str) {
get_vcm().delete_vc_by_id(uuid);
}

#[wasm_bindgen]
pub fn swap_vc_positions(ix0: usize, ix1: usize) { get_vcm().swap_vc_positions(ix0, ix1); }

#[wasm_bindgen]
pub fn switch_view_context(uuid_str: &str) {
let uuid =
Expand Down
106 changes: 59 additions & 47 deletions engine/engine/src/view_context/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,11 @@ impl ViewContextManager {
}

pub fn get_vc_by_id_mut(&mut self, uuid: Uuid) -> Option<&mut ViewContextEntry> {
self
.get_vc_position(uuid)
.map(move |ix| &mut self.contexts[ix])
self.contexts.iter_mut().find(|vc| vc.id == uuid)
}

pub fn get_vc_by_id(&self, uuid: Uuid) -> Option<&ViewContextEntry> {
self.get_vc_position(uuid).map(move |ix| &self.contexts[ix])
self.contexts.iter().find(|vc| vc.id == uuid)
}

fn init_from_state_snapshot(&mut self, vcm_state: ViewContextManagerState) {
Expand Down Expand Up @@ -790,58 +788,56 @@ impl ViewContextManager {
&serde_json::to_string(&self.subgraphs_by_id).unwrap(),
);

#[derive(Clone, Deserialize)]
struct SerializedSubgraphState {
#[serde(rename = "txSubgraphID")]
tx_subgraph_id: Uuid,
#[serde(rename = "rxSubgraphID")]
rx_subgraph_id: Uuid,
#[serde(rename = "registeredInputs")]
registered_inputs: Option<serde_json::Value>,
#[serde(rename = "registeredOutputs")]
registered_outputs: Option<serde_json::Value>,
}

let mut base_portal_state = None;
for fc in &mut serialized.fcs {
fc.subgraph_id = new_uuid_by_old_uuid[&fc.subgraph_id];

// If this is a subgraph portal linking to the connecting subgraph, we need to re-point it to
// link with the current active subgraph instead
if fc._type == "customAudio/subgraphPortal" {
// TODO: Create a struct for this
if let Some(serde_json::Value::Object(state)) = &mut fc.serialized_state {
let mapped_tx_subgraph_id = if let Some(serde_json::Value::String(tx_subgraph_id)) =
state.get_mut("txSubgraphID")
{
let mapped_id = new_uuid_by_old_uuid[&Uuid::from_str(tx_subgraph_id).unwrap()];
*tx_subgraph_id = mapped_id.to_string();
Some(mapped_id)
} else {
error!(
"Mising/invalid `txSubgraphID` in serialized state for subgraph portal FC {:?}",
state
);
None
};
if let Some(serde_json::Value::String(rx_subgraph_id)) = state.get_mut("rxSubgraphID") {
let rx_subgraph_uuid = Uuid::from_str(rx_subgraph_id).unwrap();
if fc.subgraph_id == serialized.base_subgraph_id
&& mapped_tx_subgraph_id == Some(serialized.base_subgraph_id)
&& Some(rx_subgraph_uuid) == serialized.connnecting_subgraph_id
{
info!(
"Re-pointing subgraph portal rx to active subgraph. Old ID: {}, new ID: {}",
rx_subgraph_uuid, self.active_subgraph_id
);
*rx_subgraph_id = self.active_subgraph_id.to_string();

if base_portal_state.is_some() {
error!("Found more than one subgraph portal linking to the connecting subgraph");
}
base_portal_state = Some(state.clone());
} else {
*rx_subgraph_id = new_uuid_by_old_uuid[&rx_subgraph_uuid].to_string();
}
} else {
error!(
"Mising/invalid `rxSubgraphID` in serialized state for subgraph portal FC {:?}",
state
);
}
} else {
let Some(state) = fc.serialized_state.clone() else {
error!(
"Mising/invalid serialized state for subgraph portal FC {:?}",
fc
);
continue;
};
let Ok(mut state) = serde_json::from_value::<SerializedSubgraphState>(state) else {
error!("Error deserializing subgraph portal state for FC {:?}", fc);
continue;
};

let mapped_tx_subgraph_id = new_uuid_by_old_uuid[&state.tx_subgraph_id];
state.tx_subgraph_id = mapped_tx_subgraph_id;

if fc.subgraph_id == serialized.base_subgraph_id
&& mapped_tx_subgraph_id == serialized.base_subgraph_id
&& Some(state.rx_subgraph_id) == serialized.connnecting_subgraph_id
{
info!(
"Re-pointing subgraph portal rx to active subgraph. Old ID: {}, new ID: {}",
state.rx_subgraph_id, self.active_subgraph_id
);
state.rx_subgraph_id = self.active_subgraph_id;

if base_portal_state.is_some() {
error!("Found more than one subgraph portal linking to the connecting subgraph");
}
base_portal_state = Some(state.clone());
} else {
state.rx_subgraph_id = new_uuid_by_old_uuid[&state.rx_subgraph_id];
}
}

Expand Down Expand Up @@ -946,8 +942,8 @@ impl ViewContextManager {
let rx_subgraph_id = serialized.base_subgraph_id;
let (inputs, outputs) = if let Some(base_portal_state) = base_portal_state {
(
base_portal_state.get("registeredOutputs").cloned(),
base_portal_state.get("registeredInputs").cloned(),
base_portal_state.registered_outputs,
base_portal_state.registered_inputs,
)
} else {
self.add_subgraph_portal(rx_subgraph_id, tx_subgraph_id, None, None);
Expand Down Expand Up @@ -1126,6 +1122,22 @@ impl ViewContextManager {
self.contexts.iter().position(|vc_entry| vc_entry.id == id)
}

pub fn swap_vc_positions(&mut self, a: usize, b: usize) {
self.contexts.swap(a, b);
js::set_view_contexts(
&self.active_context_id.to_string(),
&serde_json::to_string(
&self
.contexts
.iter()
.map(|vc| &vc.definition)
.collect::<Vec<_>>(),
)
.unwrap(),
);
self.save_all();
}

/// Removes the view context with the supplied ID, calling its `.cleanup()` function, deleting
/// its `localStorage` key, and updating the root `localStorage` key to no longer list it.
pub fn delete_vc_by_id(&mut self, id: Uuid) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"react": "18.3.1",
"react-ace": "^12.0.0",
"react-control-panel": ">=0.11.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.3.1",
"react-piano": "^3.1.3",
"react-query": "^3.39.3",
Expand Down
1 change: 1 addition & 0 deletions src/ViewContextManager/ViewContextManager.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ input.view-context-switcher-tab-renamer {
font-family: 'Hack', 'Input Mono', 'Input', 'Oxygen Mono', monospace;
margin-right: 4px;
margin-left: 3px;
cursor: pointer;
}

.vc-close-tab-icon:hover {
Expand Down
97 changes: 76 additions & 21 deletions src/ViewContextManager/ViewContextManager.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { shallowEqual, useSelector } from 'react-redux';
import { useDrag, useDrop, DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

import { GlobalVolumeSlider } from './GlobalVolumeSlider';
import './ViewContextManager.scss';
Expand Down Expand Up @@ -38,16 +40,19 @@ interface ViewContextIconProps extends React.HtmlHTMLAttributes<HTMLDivElement>
displayName: string | undefined;
style?: React.CSSProperties;
onClick: (evt: React.MouseEvent) => void;
fref?: React.Ref<HTMLDivElement>;
}

const ViewContextIcon: React.FC<ViewContextIconProps> = ({
displayName,
style,
onClick,
children,
fref,
...rest
}) => (
<div
ref={fref}
role='button'
title={displayName}
className='view-context-icon'
Expand Down Expand Up @@ -158,14 +163,6 @@ export const ViewContextManager: React.FC<VCMProps> = ({ engine }) => {
);
};

interface ViewContextTabProps {
engine: typeof import('src/engine');
name: string;
uuid: string;
title?: string;
active: boolean;
}

interface VCMTabRenamerProps {
value: string;
setValue: (newValue: string) => void;
Expand Down Expand Up @@ -206,20 +203,64 @@ const VCTabCloseIcon: React.FC<VCTabCloseIconProps> = ({ onClick }) => (
</div>
);

const ViewContextTab: React.FC<ViewContextTabProps> = ({ engine, name, uuid, title, active }) => {
const ItemTypes = {
TAB: 'tab',
};

interface ViewContextTabProps {
engine: typeof import('src/engine');
name: string;
uuid: string;
title?: string;
active: boolean;
index: number;
moveTab: (from: number, to: number) => void;
}

const ViewContextTab: React.FC<ViewContextTabProps> = ({
engine,
name,
uuid,
title,
active,
index,
moveTab,
}) => {
const [isRenaming, setIsRenaming] = useState(false);
const [renamingTitle, setRenamingTitle] = useState(title || '');

const displayName = title || name;

const ref = useRef(null);

const [{ isDragging }, drag] = useDrag({
type: ItemTypes.TAB,
item: { index },
collect: monitor => ({ isDragging: monitor.isDragging() }),
});

const [, drop] = useDrop({
accept: ItemTypes.TAB,
hover: (draggedItem: { index: number }) => {
if (draggedItem.index !== index) {
moveTab(draggedItem.index, index);
draggedItem.index = index;
}
},
});

drag(drop(ref));

return (
<ViewContextIcon
name={name}
displayName={displayName}
key={uuid}
fref={ref}
style={{
...styles.viewContextTab,
opacity: isDragging ? 0.5 : 1,
backgroundColor: active ? '#419282' : undefined,
cursor: 'grab',
}}
onClick={() => {
if (!active) {
Expand Down Expand Up @@ -282,29 +323,43 @@ export const ViewContextSwitcher: React.FC<ViewContextSwitcherProps> = ({ engine
shallowEqual
);
const [horizontalScroll, setHorizontalScroll] = useState(0);
const [tabs, setTabs] = useState(activeViewContexts);

useEffect(() => {
setTabs(activeViewContexts.filter(vc => vc.subgraphId === activeSubgraphID));
}, [activeViewContexts, activeSubgraphID]);

const handleScroll: React.WheelEventHandler<HTMLDivElement> = useCallback(
e => void setHorizontalScroll(prev => Math.max(prev + e.deltaY * 0.6, 0)),
[]
);

const moveTab = (fromIndex: number, toIndex: number) => {
const origFromIndex = activeViewContexts.findIndex(vc => vc.uuid === tabs[fromIndex].uuid);
const origToIndex = activeViewContexts.findIndex(vc => vc.uuid === tabs[toIndex].uuid);
engine.swap_vc_positions(origFromIndex, origToIndex);
};

return (
<div
style={{
...styles.viewContextSwitcher,
transform: `translateX(-${horizontalScroll}px)`,
}}
onWheel={handleScroll}
>
{activeViewContexts
.filter(vc => vc.subgraphId === activeSubgraphID)
.map(props => (
<DndProvider backend={HTML5Backend}>
<div
style={{
...styles.viewContextSwitcher,
transform: `translateX(-${horizontalScroll}px)`,
}}
onWheel={handleScroll}
>
{tabs.map((props, index) => (
<ViewContextTab
engine={engine}
{...props}
key={props.uuid}
active={props.uuid === activeViewContextId}
index={index}
moveTab={moveTab}
/>
))}
</div>
</div>
</DndProvider>
);
};
Loading

0 comments on commit 7172333

Please sign in to comment.