Skip to content
This repository has been archived by the owner on Jul 23, 2019. It is now read-only.

Add mouse "click and drag" to select text #107

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions xray_core/src/buffer_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ enum BufferViewAction {
SelectDown,
SelectLeft,
SelectRight,
SelectTo {
row: u32,
column: u32,
},
SelectToBeginningOfWord,
SelectToEndOfWord,
SelectToBeginningOfLine,
Expand Down Expand Up @@ -509,6 +513,20 @@ impl BufferView {
self.autoscroll_to_cursor(false);
}

pub fn select_to(&mut self, position: Point) {
self.buffer
.borrow_mut()
.mutate_selections(self.selection_set_id, |buffer, selections| {
for selection in selections.iter_mut() {
let anchor = buffer.anchor_before_point(position).unwrap();
selection.set_head(buffer, anchor);
selection.goal_column = None;
}
})
.unwrap();
self.autoscroll_to_cursor(false);
}

pub fn move_up(&mut self) {
self.buffer
.borrow_mut()
Expand Down Expand Up @@ -1086,6 +1104,10 @@ impl View for BufferView {
Ok(BufferViewAction::SelectDown) => self.select_down(),
Ok(BufferViewAction::SelectLeft) => self.select_left(),
Ok(BufferViewAction::SelectRight) => self.select_right(),
Ok(BufferViewAction::SelectTo {
row,
column
}) => self.select_to(Point::new(row, column)),
Ok(BufferViewAction::SelectToBeginningOfWord) => self.select_to_beginning_of_word(),
Ok(BufferViewAction::SelectToEndOfWord) => self.select_to_end_of_word(),
Ok(BufferViewAction::SelectToBeginningOfLine) => self.select_to_beginning_of_line(),
Expand Down Expand Up @@ -1300,6 +1322,27 @@ mod tests {
editor.move_up();
editor.move_up();
assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]);

// Select to a direct point in front of cursor position
editor.select_to(Point::new(1, 0));
assert_eq!(render_selections(&editor), vec![selection((0, 1), (1, 0))]);
editor.move_right(); // cancel selection
assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]);
editor.move_right();
editor.move_right();
assert_eq!(render_selections(&editor), vec![empty_selection(2, 1)]);

// Selection can even go to a point before the cursor (with reverse)
editor.select_to(Point::new(0, 0));
assert_eq!(render_selections(&editor), vec![rev_selection((0, 0), (2, 1))]);

// A selection can switch to a new point and the selection will update
editor.select_to(Point::new(0, 3));
assert_eq!(render_selections(&editor), vec![rev_selection((0, 3), (2, 1))]);

// A selection can even swing around the cursor without having to unselect
editor.select_to(Point::new(2, 3));
assert_eq!(render_selections(&editor), vec![selection((2, 1), (2, 3))]);
}

#[test]
Expand Down
145 changes: 121 additions & 24 deletions xray_ui/lib/text_editor/text_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ const { ActionContext, Action } = require("../action_dispatcher");

const CURSOR_BLINK_RESUME_DELAY = 300;
const CURSOR_BLINK_PERIOD = 800;
const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40;

function scaleMouseDragAutoscrollDelta (delta) {
return Math.pow(delta / 3, 3) / 280
}

const Root = styled("div", {
width: "100%",
Expand Down Expand Up @@ -38,6 +43,8 @@ class TextEditor extends React.Component {

constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseWheel = this.handleMouseWheel.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
Expand All @@ -47,7 +54,7 @@ class TextEditor extends React.Component {
CURSOR_BLINK_RESUME_DELAY
);
this.paddingLeft = 5;
this.state = { scrollLeft: 0, showLocalCursors: true };
this.state = { scrollLeft: 0, showLocalCursors: true, mouseDown: false };
}

componentDidMount() {
Expand All @@ -74,6 +81,26 @@ class TextEditor extends React.Component {
passive: true
});

let lastMousemoveEvent
const animationFrameLoop = () => {
window.requestAnimationFrame(() => {
if (this.state.mouseDown) {
this.handleMouseMove(lastMousemoveEvent)
animationFrameLoop()
}
})
}

document.addEventListener("mousemove", event => {
lastMousemoveEvent = event;
animationFrameLoop()
}, {
passive: true
});
document.addEventListener("mouseup", this.handleMouseUp, {
passive: true
});

this.startCursorBlinking();
}

Expand Down Expand Up @@ -210,21 +237,7 @@ class TextEditor extends React.Component {
);
}

handleMouseDown(event) {
if (this.canUseTextPlane()) {
this.handleClick(event);
switch (event.detail) {
case 2:
this.handleDoubleClick();
break;
case 3:
this.handleTripleClick();
break;
}
}
}

handleClick({ clientX, clientY }) {
getPositionFromMouseEvent({ clientX, clientY}) {
const { scroll_top, line_height, first_visible_row, lines } = this.props;
const { scrollLeft } = this.state;
const targetX =
Expand All @@ -245,14 +258,94 @@ class TextEditor extends React.Component {
break;
}
}
return { row, column }
}
}

this.pauseCursorBlinking();
this.props.dispatch({
type: "SetCursorPosition",
row,
column,
autoscroll: false
});
autoscrollOnMouseDrag ({clientX, clientY}) {
const top = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN
const bottom = this.props.height - MOUSE_DRAG_AUTOSCROLL_MARGIN
const left = 0 + MOUSE_DRAG_AUTOSCROLL_MARGIN
const right = this.props.width - MOUSE_DRAG_AUTOSCROLL_MARGIN

let yDelta, yDirection
if (clientY < top) {
yDelta = top - clientY
yDirection = -1
} else if (clientY > bottom) {
yDelta = clientY - bottom
yDirection = 1
}

let xDelta, xDirection
if (clientX < left) {
xDelta = left - clientX
xDirection = -1
} else if (clientX > right) {
xDelta = clientX - right
xDirection = 1
}

if (yDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection
this.updateScrollTop(scaledDelta)
}

if (xDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection
this.setScrollLeft(this.getScrollLeft() + scaledDelta)
}
}

handleMouseMove(event) {
if (this.canUseTextPlane() && this.state.mouseDown) {
const boundedPositions = {
clientX: Math.min(Math.max(event.clientX, 0), this.props.width),
clientY: Math.min(Math.max(event.clientY, 0), this.props.height),
}
this.autoscrollOnMouseDrag(event)
const pos = this.getPositionFromMouseEvent(boundedPositions);
if (pos) {
this.props.dispatch(Object.assign({
type: "SelectTo",
}, pos));
}
}
}

handleMouseUp() {
this.setState({mouseDown: false})
}

handleMouseDown(event) {
this.setState({mouseDown: true})
if (this.canUseTextPlane()) {
this.handleClick(event);
switch (event.detail) {
case 2:
this.handleDoubleClick();
break;
case 3:
this.handleTripleClick();
break;
}
}
}

handleClick(event) {
this.pauseCursorBlinking();
const pos = this.getPositionFromMouseEvent(event);
if (pos) {
if (event.shiftKey) {
this.props.dispatch(Object.assign({
type: "SelectTo"
}, pos));
} else {
this.props.dispatch(Object.assign({
type: "SetCursorPosition",
autoscroll: false
}, pos));
}
}
}

Expand All @@ -270,7 +363,7 @@ class TextEditor extends React.Component {
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
this.setScrollLeft(this.state.scrollLeft + event.deltaX);
} else {
this.props.dispatch({ type: "UpdateScrollTop", delta: event.deltaY });
this.updateScrollTop(event.deltaY);
}
}

Expand Down Expand Up @@ -368,6 +461,10 @@ class TextEditor extends React.Component {
}
}

updateScrollTop(deltaY) {
this.props.dispatch({ type: "UpdateScrollTop", delta: deltaY });
}

setScrollLeft(scrollLeft) {
this.setState({
scrollLeft: this.constrainScrollLeft(scrollLeft)
Expand Down