2 Copyright (C) 2009 Paul Davis
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 #include "ardour/session.h"
22 #include "canvas/debug.h"
24 #include <gtkmm/menu.h>
25 #include <gtkmm/menuitem.h>
27 #include "time_axis_view.h"
28 #include "streamview.h"
29 #include "editor_summary.h"
30 #include "gui_thread.h"
32 #include "region_view.h"
33 #include "rgb_macros.h"
35 #include "editor_routes.h"
36 #include "editor_cursors.h"
37 #include "mouse_cursors.h"
38 #include "route_time_axis.h"
39 #include "ui_config.h"
44 using namespace ARDOUR;
45 using Gtkmm2ext::Keyboard;
47 /** Construct an EditorSummary.
48 * @param e Editor to represent.
50 EditorSummary::EditorSummary (Editor* e)
51 : EditorComponent (e),
57 _move_dragging (false),
58 _view_rectangle_x (0, 0),
59 _view_rectangle_y (0, 0),
60 _zoom_trim_dragging (false),
61 _old_follow_playhead (false),
63 _background_dirty (true)
65 CairoWidget::use_nsglview ();
66 add_events (Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
67 set_flags (get_flags() | Gtk::CAN_FOCUS);
69 UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &EditorSummary::parameter_changed));
72 EditorSummary::~EditorSummary ()
74 cairo_surface_destroy (_image);
78 EditorSummary::parameter_changed (string p)
81 if (p == "color-regions-using-track-color") {
82 set_background_dirty ();
86 /** Handle a size allocation.
87 * @param alloc GTK allocation.
90 EditorSummary::on_size_allocate (Gtk::Allocation& alloc)
92 CairoWidget::on_size_allocate (alloc);
93 set_background_dirty ();
97 /** Connect to a session.
101 EditorSummary::set_session (Session* s)
103 SessionHandlePtr::set_session (s);
107 /* Note: the EditorSummary already finds out about new regions from Editor::region_view_added
108 * (which attaches to StreamView::RegionViewAdded), and cut regions by the RegionPropertyChanged
109 * emitted when a cut region is added to the `cutlist' playlist.
113 Region::RegionPropertyChanged.connect (region_property_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
114 PresentationInfo::Change.connect (route_ctrl_id_connection, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
115 _editor->playhead_cursor->PositionChanged.connect (position_connection, invalidator (*this), boost::bind (&EditorSummary::playhead_position_changed, this, _1), gui_context());
116 _session->StartTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
117 _session->EndTimeChanged.connect (_session_connections, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context());
118 _editor->selection->RegionsChanged.connect (sigc::mem_fun(*this, &EditorSummary::set_background_dirty));
121 _leftmost = max_samplepos;
126 EditorSummary::render_background_image ()
128 cairo_surface_destroy (_image); // passing NULL is safe
129 _image = cairo_image_surface_create (CAIRO_FORMAT_RGB24, get_width (), get_height ());
131 cairo_t* cr = cairo_create (_image);
133 /* background (really just the dividing lines between tracks */
135 cairo_set_source_rgb (cr, 0, 0, 0);
136 cairo_rectangle (cr, 0, 0, get_width(), get_height());
139 /* compute start and end points for the summary */
141 std::pair<samplepos_t, samplepos_t> ext = _editor->session_gui_extents();
142 double theoretical_start = ext.first;
143 double theoretical_end = ext.second;
145 /* the summary should encompass the full extent of everywhere we've visited since the session was opened */
146 if (_leftmost < theoretical_start)
147 theoretical_start = _leftmost;
148 if (_rightmost > theoretical_end)
149 theoretical_end = _rightmost;
152 _start = theoretical_start > 0 ? theoretical_start : 0;
153 _end = theoretical_end < max_samplepos ? theoretical_end : max_samplepos;
155 /* calculate x scale */
156 if (_end != _start) {
157 _x_scale = static_cast<double> (get_width()) / (_end - _start);
162 /* compute track height */
164 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
165 if (!(*i)->hidden()) {
173 _track_height = (double) get_height() / N;
176 /* render tracks and regions */
179 for (TrackViewList::const_iterator i = _editor->track_views.begin(); i != _editor->track_views.end(); ++i) {
181 if ((*i)->hidden()) {
185 /* paint a non-bg colored strip to represent the track itself */
187 if (_track_height > 4) {
188 cairo_set_source_rgb (cr, 0.2, 0.2, 0.2);
189 cairo_set_line_width (cr, _track_height - 1);
190 cairo_move_to (cr, 0, y + _track_height / 2);
191 cairo_line_to (cr, get_width(), y + _track_height / 2);
195 StreamView* s = (*i)->view ();
198 cairo_set_line_width (cr, _track_height * 0.8);
200 s->foreach_regionview (sigc::bind (
201 sigc::mem_fun (*this, &EditorSummary::render_region),
203 y + _track_height / 2
210 /* start and end markers */
212 cairo_set_line_width (cr, 1);
213 cairo_set_source_rgb (cr, 1, 1, 0);
215 const double p = (_session->current_start_sample() - _start) * _x_scale;
216 cairo_move_to (cr, p, 0);
217 cairo_line_to (cr, p, get_height());
219 double const q = (_session->current_end_sample() - _start) * _x_scale;
220 cairo_move_to (cr, q, 0);
221 cairo_line_to (cr, q, get_height());
227 /** Render the required regions to a cairo context.
231 EditorSummary::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
233 cairo_t* cr = ctx->cobj();
239 /* maintain the leftmost and rightmost locations that we've ever reached */
240 samplecnt_t const leftmost = _editor->leftmost_sample ();
241 if (leftmost < _leftmost) {
242 _leftmost = leftmost;
243 _background_dirty = true;
245 samplecnt_t const rightmost = leftmost + _editor->current_page_samples();
246 if (rightmost > _rightmost) {
247 _rightmost = rightmost;
248 _background_dirty = true;
251 /* draw the background (regions, markers, etc) if they've changed */
252 if (!_image || _background_dirty) {
253 render_background_image ();
254 _background_dirty = false;
257 cairo_push_group (cr);
259 /* Fill with the background image */
261 cairo_rectangle (cr, 0, 0, get_width(), get_height());
262 cairo_set_source_surface (cr, _image, 0, 0);
265 /* Render the view rectangle. If there is an editor visual pending, don't update
266 * the view rectangle now --- wait until the expose event that we'll get after
267 * the visual change. This prevents a flicker.
270 if (_editor->pending_visual_change.idle_handler_id < 0) {
271 get_editor (&_view_rectangle_x, &_view_rectangle_y);
274 int32_t width = _view_rectangle_x.second - _view_rectangle_x.first;
276 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
277 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
281 cairo_rectangle (cr, _view_rectangle_x.first, 0, width, get_height ());
282 cairo_set_line_width (cr, 1);
283 cairo_set_source_rgba (cr, 1, 1, 1, 0.9);
288 cairo_set_line_width (cr, 1);
289 /* XXX: colour should be set from configuration file */
290 cairo_set_source_rgba (cr, 1, 0, 0, 1);
292 const double ph= playhead_sample_to_position (_editor->playhead_cursor->current_sample());
293 cairo_move_to (cr, ph, 0);
294 cairo_line_to (cr, ph, get_height());
296 cairo_pop_group_to_source (cr);
302 /** Render a region for the summary.
303 * @param r Region view.
304 * @param cr Cairo context.
305 * @param y y coordinate to render at.
308 EditorSummary::render_region (RegionView* r, cairo_t* cr, double y) const
310 /*NOTE: you can optimize this operation by coalescing adjacent regions into a single line stroke.
311 * In a session with a single track ~1,000 regions, this reduced render time from 14ms to 11 ms.
312 * However, you lose a lot of visual information. The current method preserves a sense of separation between regions.
313 * The current method shows the current selection (red regions), which needs to be preserved if this is optimized.
314 * I think it's not worth it for now, but we might choose to revisit this someday.
317 uint32_t const c = r->get_fill_color ();
318 cairo_set_source_rgb (cr, UINT_RGBA_R (c) / 255.0, UINT_RGBA_G (c) / 255.0, UINT_RGBA_B (c) / 255.0);
320 if (r->region()->position() > _start) {
321 cairo_move_to (cr, (r->region()->position() - _start) * _x_scale, y);
323 cairo_move_to (cr, 0, y);
326 if ((r->region()->position() + r->region()->length()) > _start) {
327 cairo_line_to (cr, ((r->region()->position() - _start + r->region()->length())) * _x_scale, y);
329 cairo_line_to (cr, 0, y);
336 EditorSummary::set_background_dirty ()
338 if (!_background_dirty) {
339 _background_dirty = true;
344 /** Set the summary so that just the overlays (viewbox, playhead etc.) will be re-rendered */
346 EditorSummary::set_overlays_dirty ()
348 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty);
352 /** Set the summary so that just the overlays (viewbox, playhead etc.) in a given area will be re-rendered */
354 EditorSummary::set_overlays_dirty_rect (int x, int y, int w, int h)
356 ENSURE_GUI_THREAD (*this, &EditorSummary::set_overlays_dirty_rect);
357 queue_draw_area (x, y, w, h);
361 /** Handle a size request.
362 * @param req GTK requisition
365 EditorSummary::on_size_request (Gtk::Requisition *req)
367 /* The left/right buttons will determine our height */
374 EditorSummary::centre_on_click (GdkEventButton* ev)
376 pair<double, double> xr;
379 double const w = xr.second - xr.first;
380 double ex = ev->x - w / 2;
383 } else if ((ex + w) > get_width()) {
384 ex = get_width() - w;
391 EditorSummary::on_enter_notify_event (GdkEventCrossing*)
394 Keyboard::magic_widget_grab_focus ();
399 EditorSummary::on_leave_notify_event (GdkEventCrossing*)
401 /* there are no inferior/child windows, so any leave event means that
404 Keyboard::magic_widget_drop_focus ();
409 EditorSummary::on_key_press_event (GdkEventKey* key)
412 GtkAccelKey set_playhead_accel;
413 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
414 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
417 _session->request_locate (_start + (samplepos_t) x / _x_scale, _session->transport_rolling());
427 EditorSummary::on_key_release_event (GdkEventKey* key)
430 GtkAccelKey set_playhead_accel;
431 if (gtk_accel_map_lookup_entry ("<Actions>/Editor/set-playhead", &set_playhead_accel)) {
432 if (key->keyval == set_playhead_accel.accel_key && (int) key->state == set_playhead_accel.accel_mods) {
439 #include "gtkmm2ext/utils.h"
441 /** Handle a button press.
442 * @param ev GTK event.
445 EditorSummary::on_button_press_event (GdkEventButton* ev)
447 _old_follow_playhead = _editor->follow_playhead ();
449 if (ev->button == 3) { // right-click: show the reset menu action
450 using namespace Gtk::Menu_Helpers;
451 Gtk::Menu* m = manage (new Gtk::Menu);
452 MenuList& items = m->items ();
453 items.push_back(MenuElem(_("Reset Summary to Extents"),
454 sigc::mem_fun(*this, &EditorSummary::reset_to_extents)));
455 m->popup (ev->button, ev->time);
459 if (ev->button != 1) {
463 pair<double, double> xr;
466 _start_editor_x = xr;
467 _start_mouse_x = ev->x;
468 _start_mouse_y = ev->y;
469 _start_position = get_position (ev->x, ev->y);
471 if (_start_position != INSIDE && _start_position != TO_LEFT_OR_RIGHT) {
473 /* start a zoom_trim drag */
475 _zoom_trim_position = get_position (ev->x, ev->y);
476 _zoom_trim_dragging = true;
477 _editor->_dragging_playhead = true;
478 _editor->set_follow_playhead (false);
480 if (suspending_editor_updates ()) {
481 get_editor (&_pending_editor_x, &_pending_editor_y);
482 _pending_editor_changed = false;
485 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
487 /* secondary-modifier-click: locate playhead */
489 _session->request_locate (ev->x / _x_scale + _start);
492 } else if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
494 centre_on_click (ev);
498 /* start a move+zoom drag */
499 get_editor (&_pending_editor_x, &_pending_editor_y);
500 _pending_editor_changed = false;
501 _editor->_dragging_playhead = true;
502 _editor->set_follow_playhead (false);
504 _move_dragging = true;
512 get_window()->set_cursor (*_editor->_cursors->expand_left_right);
519 /** @return true if we are currently suspending updates to the editor's viewport,
520 * which we do if configured to do so, and if in a drag of some kind.
523 EditorSummary::suspending_editor_updates () const
525 return (!UIConfiguration::instance().get_update_editor_during_summary_drag () && (_zoom_trim_dragging || _move_dragging));
528 /** Fill in x and y with the editor's current viewable area in summary coordinates */
530 EditorSummary::get_editor (pair<double, double>* x, pair<double, double>* y) const
533 if (suspending_editor_updates ()) {
535 /* We are dragging, and configured not to update the editor window during drags,
536 * so just return where the editor will be when the drag finishes.
539 *x = _pending_editor_x;
541 *y = _pending_editor_y;
546 /* Otherwise query the editor for its actual position */
548 x->first = (_editor->leftmost_sample () - _start) * _x_scale;
549 x->second = x->first + _editor->current_page_samples() * _x_scale;
552 y->first = editor_y_to_summary (_editor->vertical_adjustment.get_value ());
553 y->second = editor_y_to_summary (_editor->vertical_adjustment.get_value () + _editor->visible_canvas_height() - _editor->get_trackview_group()->canvas_origin().y);
557 /** Get an expression of the position of a point with respect to the view rectangle */
558 EditorSummary::Position
559 EditorSummary::get_position (double x, double y) const
561 /* how close the mouse has to be to the edge of the view rectangle to be considered `on it',
564 int x_edge_size = (_view_rectangle_x.second - _view_rectangle_x.first) / 4;
565 x_edge_size = min (x_edge_size, 8);
566 x_edge_size = max (x_edge_size, 1);
568 bool const near_left = (std::abs (x - _view_rectangle_x.first) < x_edge_size);
569 bool const near_right = (std::abs (x - _view_rectangle_x.second) < x_edge_size);
570 bool const within_x = _view_rectangle_x.first < x && x < _view_rectangle_x.second;
574 } else if (near_right) {
576 } else if (within_x) {
579 return TO_LEFT_OR_RIGHT;
584 EditorSummary::reset_to_extents()
586 /* reset as if the user never went anywhere outside the extents */
587 _leftmost = max_samplepos;
590 _editor->temporal_zoom_extents ();
591 set_background_dirty ();
596 EditorSummary::set_cursor (Position p)
600 get_window()->set_cursor (*_editor->_cursors->resize_left);
603 get_window()->set_cursor (*_editor->_cursors->resize_right);
606 get_window()->set_cursor (*_editor->_cursors->move);
608 case TO_LEFT_OR_RIGHT:
609 get_window()->set_cursor (*_editor->_cursors->move);
613 get_window()->set_cursor ();
619 EditorSummary::summary_zoom_step (int steps /* positive steps to zoom "out" , negative steps to zoom "in" */ )
621 pair<double, double> xn;
628 /* for now, disallow really close zooming-in from the scroomer. (Currently it
629 * causes the start-offset to 'walk' because of integer limitations.
630 * To fix this, probably need to maintain float throught the get/set_editor() path.)
633 if ((xn.second - xn.first) < 2)
637 set_overlays_dirty ();
643 EditorSummary::on_motion_notify_event (GdkEventMotion* ev)
645 if (_move_dragging) {
647 /* To avoid accidental zooming, the mouse must move exactly vertical, not diagonal, to trigger a zoom step
648 * we use screen coordinates for this, not canvas-based grab_x */
650 double dx = mx - _last_mx;
652 double dy = my - _last_my;
654 /* do zooming in windowed "steps" so it feels more reversible ? */
655 const int stepsize = 2;
656 int y_delta = _start_mouse_y - my;
657 y_delta = y_delta / stepsize;
660 const float zscale = 3;
661 if ((dx == 0) && (_last_dx == 0) && (y_delta != _last_y_delta)) {
663 summary_zoom_step (dy * zscale);
665 /* after the zoom we must re-calculate x-pos grabs */
666 pair<double, double> xr;
668 _start_editor_x = xr;
669 _start_mouse_x = ev->x;
671 _last_y_delta = y_delta;
674 /* always track horizontal movement, if any */
677 double x = _start_editor_x.first;
678 x += ev->x - _start_mouse_x;
684 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the end */
685 pair<double, double> xr;
687 double w = xr.second - xr.first;
688 if (x + w < get_width()) {
698 } else if (_zoom_trim_dragging) {
700 pair<double, double> xr = _start_editor_x;
702 double const dx = ev->x - _start_mouse_x;
704 if (_zoom_trim_position == LEFT) {
706 } else if (_zoom_trim_position == RIGHT) {
708 /* zoom-behavior-tweaks: protect the right edge from expanding beyond the edge */
709 if ((xr.second + dx) < get_width()) {
715 xr.first = -1; /* do not change */
718 set_overlays_dirty ();
719 set_cursor (_zoom_trim_position);
723 set_cursor (get_position (ev->x, ev->y));
730 EditorSummary::on_button_release_event (GdkEventButton*)
732 bool const was_suspended = suspending_editor_updates ();
734 _move_dragging = false;
735 _zoom_trim_dragging = false;
736 _editor->_dragging_playhead = false;
737 _editor->set_follow_playhead (_old_follow_playhead, false);
739 if (was_suspended && _pending_editor_changed) {
740 set_editor (_pending_editor_x);
747 EditorSummary::on_scroll_event (GdkEventScroll* ev)
750 pair<double, double> xr;
754 switch (ev->direction) {
755 case GDK_SCROLL_UP: {
757 summary_zoom_step (-4);
762 case GDK_SCROLL_DOWN: {
764 summary_zoom_step (4);
769 case GDK_SCROLL_LEFT:
770 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
771 _editor->temporal_zoom_step (false);
772 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
774 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
777 _editor->scroll_left_half_page ();
781 case GDK_SCROLL_RIGHT:
782 if (Keyboard::modifier_state_equals (ev->state, Keyboard::ScrollZoomHorizontalModifier)) {
783 _editor->temporal_zoom_step (true);
784 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
786 } else if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
789 _editor->scroll_right_half_page ();
801 /** Set the editor to display a x range with the left at a given position
802 * and a y range with the top at a given position.
803 * x and y parameters are specified in summary coordinates.
804 * Zoom is not changed in either direction.
807 EditorSummary::set_editor (double const x)
809 if (_editor->pending_visual_change.idle_handler_id >= 0 && _editor->pending_visual_change.being_handled == true) {
811 /* As a side-effect, the Editor's visual change idle handler processes
812 pending GTK events. Hence this motion notify handler can be called
813 in the middle of a visual change idle handler, and if this happens,
814 the queue_visual_change calls below modify the variables that the
815 idle handler is working with. This causes problems. Hence this
816 check. It ensures that we won't modify the pending visual change
817 while a visual change idle handler is in progress. It's not perfect,
818 as it also means that we won't change these variables if an idle handler
819 is merely pending but not executing. But c'est la vie.
828 /** Set the editor to display a given x range and a y range with the top at a given position.
829 * The editor's x zoom is adjusted if necessary, but the y zoom is not changed.
830 * x and y parameters are specified in summary coordinates.
833 EditorSummary::set_editor (pair<double,double> const x)
835 if (_editor->pending_visual_change.idle_handler_id >= 0) {
836 /* see comment in other set_editor () */
845 /** Set the left of the x range visible in the editor.
846 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
847 * @param x new x left position in summary coordinates.
850 EditorSummary::set_editor_x (double x)
856 if (suspending_editor_updates ()) {
857 double const w = _pending_editor_x.second - _pending_editor_x.first;
858 _pending_editor_x.first = x;
859 _pending_editor_x.second = x + w;
860 _pending_editor_changed = true;
863 _editor->reset_x_origin (x / _x_scale + _start);
867 /** Set the x range visible in the editor.
868 * Caller should have checked that Editor::pending_visual_change.idle_handler_id is < 0
869 * @param x new x range in summary coordinates.
872 EditorSummary::set_editor_x (pair<double, double> x)
879 x.second = x.first + 1;
882 if (suspending_editor_updates ()) {
883 _pending_editor_x = x;
884 _pending_editor_changed = true;
887 _editor->reset_x_origin (x.first / _x_scale + _start);
890 ((x.second - x.first) / _x_scale) /
891 _editor->sample_to_pixel (_editor->current_page_samples())
894 if (nx != _editor->get_current_zoom ()) {
895 _editor->reset_zoom (nx);
901 EditorSummary::playhead_position_changed (samplepos_t p)
903 int const o = int (_last_playhead);
904 int const n = int (playhead_sample_to_position (p));
905 if (_session && o != n) {
906 int a = max(2, min (o, n));
908 set_overlays_dirty_rect (a - 2, 0, b + 2, get_height ());
913 EditorSummary::editor_y_to_summary (double y) const
916 for (TrackViewList::const_iterator i = _editor->track_views.begin (); i != _editor->track_views.end(); ++i) {
918 if ((*i)->hidden()) {
922 double const h = (*i)->effective_height ();
925 return sy + y * _track_height / h;
936 EditorSummary::routes_added (list<RouteTimeAxisView*> const & r)
938 for (list<RouteTimeAxisView*>::const_iterator i = r.begin(); i != r.end(); ++i) {
939 /* Connect to the relevant signal for the route so that we know when its colour has changed */
940 (*i)->route()->presentation_info().PropertyChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::route_gui_changed, this, _1), gui_context ());
941 boost::shared_ptr<Track> tr = boost::dynamic_pointer_cast<Track> ((*i)->route ());
943 tr->PlaylistChanged.connect (*this, invalidator (*this), boost::bind (&EditorSummary::set_background_dirty, this), gui_context ());
947 set_background_dirty ();
951 EditorSummary::route_gui_changed (PBD::PropertyChange const& what_changed)
953 if (what_changed.contains (Properties::color)) {
954 set_background_dirty ();
959 EditorSummary::playhead_sample_to_position (samplepos_t t) const
961 return (t - _start) * _x_scale;
965 EditorSummary::position_to_playhead_sample_to_position (double pos) const
967 return _start + (pos * _x_scale);