Skip to content
This repository was 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
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
@@ -70,6 +70,10 @@ enum BufferViewAction {
SelectDown,
SelectLeft,
SelectRight,
SelectTo {
row: u32,
column: u32,
},
SelectToBeginningOfWord,
SelectToEndOfWord,
SelectToBeginningOfLine,
@@ -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()
@@ -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(),
@@ -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]
145 changes: 121 additions & 24 deletions xray_ui/lib/text_editor/text_editor.js
Original file line number Diff line number Diff line change
@@ -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%",
@@ -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);
@@ -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() {
@@ -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();
}

@@ -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 =
@@ -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));
}
}
}

@@ -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);
}
}

@@ -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)