| #include "rive/text/text_selection_controller.hpp" | |
| #ifdef WITH_RIVE_TEXT | |
| #include "rive/artboard.hpp" | |
| #include "rive/nested_artboard.hpp" | |
| #include "rive/artboard_component_list.hpp" | |
| #include "rive/factory.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/text/text.hpp" | |
| #include "rive/text/text_selection_path.hpp" | |
| #include "rive/text/utf.hpp" | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <limits> | |
| using namespace rive; | |
| struct TextSelectionController::Participant | |
| { | |
| Text* text; | |
| uint32_t id = 0; | |
| std::string separatorBefore; | |
| std::vector<Unichar> source; | |
| GlyphLookup lookup; | |
| Cursor cursor = Cursor::zero(); | |
| std::vector<AABB> rects; | |
| TextSelectionPath path; | |
| rcp<RenderPaint> paint; | |
| bool lookupDirty = true; | |
| bool geometryDirty = true; | |
| float radius = -1; | |
| #ifdef TESTING | |
| size_t lookupBuilds = 0; | |
| #endif | |
| Participant(Text* text, std::string separator) | |
| : text(text) | |
| , separatorBefore(std::move(separator)) | |
| {} | |
| }; | |
| // rootTransform includes every nested host and its artboard's self transform. | |
| // Sampling the affine basis also covers Dart-mounted artboard callbacks. | |
| static Mat2D shapeToRoot(Text* text) | |
| { | |
| auto* artboard = text->artboard(); | |
| auto origin = artboard->rootTransform(Vec2D(0, 0)); | |
| auto x = artboard->rootTransform(Vec2D(1, 0)) - origin; | |
| auto y = artboard->rootTransform(Vec2D(0, 1)) - origin; | |
| return Mat2D(x.x, x.y, y.x, y.y, origin.x, origin.y) * | |
| text->shapeWorldTransform(); | |
| } | |
| static bool finite(Vec2D position) | |
| { | |
| return std::isfinite(position.x) && std::isfinite(position.y); | |
| } | |
| static AABB lineBounds(const OrderedLine& line) | |
| { | |
| float x = line.startX(); | |
| auto bounds = AABB::forExpansion(); | |
| for (auto glyph : line) | |
| { | |
| auto* run = std::get<0>(glyph); | |
| auto index = std::get<1>(glyph); | |
| AABB::expandTo(bounds, Vec2D(x, line.y() + run->font->ascent(run->size))); | |
| x += run->advances[index]; | |
| AABB::expandTo(bounds, Vec2D(x, line.y() + run->font->descent(run->size))); | |
| } | |
| return bounds; | |
| } | |
| TextSelectionController::TextSelectionController() = default; | |
| TextSelectionController::~TextSelectionController() | |
| { | |
| for (auto& participant : m_participants) | |
| { | |
| participant->text->m_selectionController = nullptr; | |
| } | |
| } | |
| TextSelectionController::Participant* TextSelectionController::find( | |
| Text* text) const | |
| { | |
| for (auto& participant : m_participants) | |
| { | |
| if (participant->text == text) | |
| { | |
| return participant.get(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool TextSelectionController::add(Text* text, std::string separatorBefore) | |
| { | |
| if (text == nullptr || text->artboard() == nullptr || | |
| text->m_selectionController != nullptr || text->haveModifiers() || | |
| text->overflow() == TextOverflow::ellipsis) | |
| { | |
| return false; | |
| } | |
| clear(); | |
| auto participant = std::make_unique<Participant>( | |
| text, | |
| std::move(separatorBefore)); | |
| participant->source = text->m_styledText.unichars(); | |
| participant->id = m_nextTargetId++; | |
| text->m_selectionController = this; | |
| m_participants.push_back(std::move(participant)); | |
| return true; | |
| } | |
| void TextSelectionController::remove(Text* text) | |
| { | |
| auto it = std::find_if( | |
| m_participants.begin(), | |
| m_participants.end(), | |
| [text](const std::unique_ptr<Participant>& p) { | |
| return p->text == text; | |
| }); | |
| if (it == m_participants.end()) | |
| { | |
| return; | |
| } | |
| clear(); | |
| text->m_selectionController = nullptr; | |
| m_participants.erase(it); | |
| } | |
| bool TextSelectionController::eligible(const Participant& participant) const | |
| { | |
| auto* text = participant.text; | |
| return !text->isHidden() && text->renderOpacity() > 0 && | |
| !text->haveModifiers() && text->overflow() != TextOverflow::ellipsis && | |
| !text->m_orderedLines.empty() && !participant.source.empty(); | |
| } | |
| static void collectSelectable(Artboard* artboard, std::vector<Text*>& result) | |
| { | |
| if (artboard == nullptr) | |
| return; | |
| // Every Text is a Drawable, and drawables() keeps their relative object | |
| // order while skipping the vertices, paints and other non-drawables that | |
| // make up most of objects(). Hosts synchronize on every pointer move. | |
| for (auto* drawable : artboard->drawables()) | |
| { | |
| if (drawable->is<Text>() && drawable->isSelectable()) | |
| result.push_back(drawable->as<Text>()); | |
| } | |
| for (auto* nested : artboard->nestedArtboards()) | |
| collectSelectable(nested->artboardInstance(), result); | |
| for (auto* list : artboard->artboardComponentLists()) | |
| for (size_t i = 0; i < list->artboardCount(); ++i) | |
| collectSelectable(list->artboardInstance((int)i), result); | |
| } | |
| void TextSelectionController::synchronize(Artboard* root) | |
| { | |
| m_styleFile = root != nullptr && root->isInstance() | |
| ? static_cast<ArtboardInstance*>(root)->file() | |
| : nullptr; | |
| std::vector<Text*> texts; | |
| collectSelectable(root, texts); | |
| std::vector<Text*> removed; | |
| for (const auto& p : m_participants) | |
| if (std::find(texts.begin(), texts.end(), p->text) == texts.end()) | |
| removed.push_back(p->text); | |
| for (auto* text : removed) | |
| remove(text); | |
| for (auto* text : texts) | |
| if (find(text) == nullptr) | |
| add(text); | |
| } | |
| std::vector<uint32_t> TextSelectionController::targetIds() const | |
| { | |
| std::vector<uint32_t> ids; | |
| for (const auto& p : m_participants) | |
| ids.push_back(p->id); | |
| return ids; | |
| } | |
| Text* TextSelectionController::target(uint32_t id) const | |
| { | |
| for (const auto& p : m_participants) | |
| if (p->id == id) | |
| return p->text; | |
| return nullptr; | |
| } | |
| bool TextSelectionController::isEligible(Text* text) const | |
| { | |
| auto* p = find(text); | |
| return p != nullptr && eligible(*p); | |
| } | |
| std::string TextSelectionController::sourceText(Text* text) const | |
| { | |
| auto* p = find(text); | |
| std::string result; | |
| if (p == nullptr) | |
| return result; | |
| for (auto ch : p->source) | |
| { | |
| uint8_t bytes[4]; | |
| auto count = UTF::Encode(bytes, ch); | |
| result.append(reinterpret_cast<char*>(bytes), count); | |
| } | |
| return result; | |
| } | |
| AABB TextSelectionController::rootBounds(Text* text) const | |
| { | |
| auto* p = find(text); | |
| AABB result = AABB::forExpansion(); | |
| if (p == nullptr || !eligible(*p)) | |
| return result; | |
| auto transform = shapeToRoot(text); | |
| for (const auto& line : text->m_orderedLines) | |
| { | |
| auto b = lineBounds(line); | |
| if (b.isEmptyOrNaN()) | |
| continue; | |
| AABB::expandTo(result, transform * Vec2D(b.minX, b.minY)); | |
| AABB::expandTo(result, transform * Vec2D(b.maxX, b.minY)); | |
| AABB::expandTo(result, transform * Vec2D(b.minX, b.maxY)); | |
| AABB::expandTo(result, transform * Vec2D(b.maxX, b.maxY)); | |
| } | |
| return result; | |
| } | |
| bool TextSelectionController::hitTest( | |
| Text* text, | |
| Vec2D point, | |
| bool nearest, | |
| uint32_t& offset, | |
| float& distanceSquared) | |
| { | |
| auto* p = find(text); | |
| if (p == nullptr || !eligible(*p) || !finite(point)) | |
| return false; | |
| auto transform = shapeToRoot(text); | |
| Mat2D inverse; | |
| if (!transform.invert(&inverse)) | |
| return false; | |
| auto local = inverse * point; | |
| if (!finite(local)) | |
| return false; | |
| if (!nearest) | |
| { | |
| auto artboardPoint = text->shapeWorldTransform() * local; | |
| if (!text->hitTestPoint(artboardPoint, false, true)) | |
| return false; | |
| if (text->overflow() == TextOverflow::clipped) | |
| { | |
| Mat2D inverseWorld; | |
| if (!text->worldTransform().invert(&inverseWorld) || | |
| !text->localBounds().contains(inverseWorld * artboardPoint)) | |
| return false; | |
| } | |
| } | |
| bool found = false; | |
| distanceSquared = std::numeric_limits<float>::infinity(); | |
| uint32_t index = 0; | |
| for (const auto& line : text->m_orderedLines) | |
| { | |
| auto b = lineBounds(line); | |
| if (!b.isEmptyOrNaN() && (nearest || b.contains(local))) | |
| { | |
| Vec2D closest( | |
| std::max(b.minX, std::min(local.x, b.maxX)), | |
| std::max(b.minY, std::min(local.y, b.maxY))); | |
| auto delta = transform * closest - point; | |
| auto distance = delta.x * delta.x + delta.y * delta.y; | |
| if (distance < distanceSquared) | |
| { | |
| distanceSquared = distance; | |
| offset = CursorPosition::fromLineX(index, local.x, layout(*p)) | |
| .codePointIndex(); | |
| found = true; | |
| } | |
| } | |
| ++index; | |
| } | |
| return found; | |
| } | |
| void TextSelectionController::setRange(Text* text, uint32_t start, uint32_t end) | |
| { | |
| auto* p = find(text); | |
| if (p == nullptr) | |
| return; | |
| if (!m_externalRanges) | |
| clear(); | |
| m_externalRanges = true; | |
| auto length = (uint32_t)p->source.size(); | |
| start = std::min(start, length); | |
| end = std::min(end, length); | |
| if (p->cursor.first().codePointIndex() == std::min(start, end) && | |
| p->cursor.last().codePointIndex() == std::max(start, end)) | |
| return; | |
| p->cursor = Cursor(CursorPosition(start), CursorPosition(end)); | |
| p->geometryDirty = true; | |
| } | |
| TextLayoutView TextSelectionController::layout(Participant& participant) | |
| { | |
| auto* text = participant.text; | |
| if (participant.lookupDirty) | |
| { | |
| participant.lookup.compute(text->m_styledText.unichars(), text->m_shape); | |
| participant.lookupDirty = false; | |
| #ifdef TESTING | |
| participant.lookupBuilds++; | |
| #endif | |
| } | |
| return TextLayoutView( | |
| text->m_shape, | |
| text->m_lines, | |
| text->m_orderedLines, | |
| participant.lookup, | |
| (uint32_t)participant.source.size()); | |
| } | |
| void TextSelectionController::textUpdated(Text* text, bool shapeChanged) | |
| { | |
| auto* participant = find(text); | |
| if (participant == nullptr) | |
| { | |
| return; | |
| } | |
| if (shapeChanged) | |
| { | |
| const auto& source = text->m_styledText.unichars(); | |
| if (participant->source != source) | |
| { | |
| clear(); | |
| participant->source = source; | |
| } | |
| participant->lookupDirty = true; | |
| } | |
| participant->geometryDirty = true; | |
| if (m_anchor.participant != nullptr && !validSelection()) | |
| { | |
| clear(); | |
| } | |
| } | |
| #ifdef TESTING | |
| size_t TextSelectionController::lookupBuildCount(Text* text) const | |
| { | |
| auto* participant = find(text); | |
| return participant == nullptr ? 0 : participant->lookupBuilds; | |
| } | |
| #endif | |
| bool TextSelectionController::validSelection() const | |
| { | |
| if (m_externalRanges) | |
| { | |
| for (auto& participant : m_participants) | |
| { | |
| if (participant->cursor.hasSelection() && !eligible(*participant)) | |
| return false; | |
| } | |
| return true; | |
| } | |
| if (m_anchor.participant == nullptr || m_extent.participant == nullptr || | |
| !eligible(*m_anchor.participant) || !eligible(*m_extent.participant)) | |
| { | |
| return false; | |
| } | |
| for (auto& participant : m_participants) | |
| { | |
| if (participant->cursor.first().codePointIndex() != | |
| participant->cursor.last().codePointIndex() && | |
| !eligible(*participant)) | |
| { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void TextSelectionController::clear() | |
| { | |
| m_anchor = {}; | |
| m_extent = {}; | |
| m_dragging = false; | |
| m_externalRanges = false; | |
| for (auto& participant : m_participants) | |
| { | |
| participant->cursor = Cursor::zero(); | |
| participant->geometryDirty = true; | |
| } | |
| } | |
| bool TextSelectionController::endpointAt( | |
| Participant& participant, | |
| Vec2D rootPosition, | |
| Endpoint& out) | |
| { | |
| Mat2D inverse; | |
| if (!eligible(participant) || !finite(rootPosition) || | |
| !shapeToRoot(participant.text).invert(&inverse)) | |
| { | |
| return false; | |
| } | |
| auto local = inverse * rootPosition; | |
| if (!finite(local)) | |
| { | |
| return false; | |
| } | |
| out = { | |
| &participant, | |
| CursorPosition::fromTranslation(local, layout(participant))}; | |
| return true; | |
| } | |
| bool TextSelectionController::pointerDown( | |
| Text* hitText, | |
| Vec2D rootPosition, | |
| int pointerId, | |
| bool extend) | |
| { | |
| // A second pointer must not replace the anchor or take over capture. | |
| if (m_dragging) | |
| { | |
| return false; | |
| } | |
| auto* participant = find(hitText); | |
| Endpoint endpoint; | |
| if (participant == nullptr || | |
| !endpointAt(*participant, rootPosition, endpoint)) | |
| { | |
| clear(); | |
| return false; | |
| } | |
| if (!extend || m_externalRanges || !validSelection()) | |
| { | |
| m_anchor = endpoint; | |
| } | |
| m_externalRanges = false; | |
| m_extent = endpoint; | |
| m_pointerId = pointerId; | |
| m_dragging = true; | |
| updateRanges(); | |
| return true; | |
| } | |
| bool TextSelectionController::nearestEndpoint(Vec2D rootPosition, Endpoint& out) | |
| { | |
| if (!finite(rootPosition)) | |
| { | |
| return false; | |
| } | |
| float bestDistance = std::numeric_limits<float>::infinity(); | |
| bool found = false; | |
| for (auto& participant : m_participants) | |
| { | |
| Mat2D inverse; | |
| auto transform = shapeToRoot(participant->text); | |
| if (!eligible(*participant) || !transform.invert(&inverse)) | |
| { | |
| continue; | |
| } | |
| auto local = inverse * rootPosition; | |
| if (!finite(local)) | |
| { | |
| continue; | |
| } | |
| uint32_t lineIndex = 0; | |
| for (const auto& line : participant->text->m_orderedLines) | |
| { | |
| auto bounds = lineBounds(line); | |
| if (!bounds.isEmptyOrNaN()) | |
| { | |
| Vec2D closest( | |
| std::max(bounds.minX, std::min(local.x, bounds.maxX)), | |
| std::max(bounds.minY, std::min(local.y, bounds.maxY))); | |
| auto delta = transform * closest - rootPosition; | |
| float distance = delta.x * delta.x + delta.y * delta.y; | |
| if (distance < bestDistance) | |
| { | |
| bestDistance = distance; | |
| out = { | |
| participant.get(), | |
| CursorPosition::fromLineX( | |
| lineIndex, | |
| local.x, | |
| layout(*participant))}; | |
| found = true; | |
| } | |
| } | |
| lineIndex++; | |
| } | |
| } | |
| return found; | |
| } | |
| bool TextSelectionController::pointerMove(Vec2D rootPosition, int pointerId) | |
| { | |
| if (!m_dragging || pointerId != m_pointerId) | |
| { | |
| return false; | |
| } | |
| if (!validSelection()) | |
| { | |
| clear(); | |
| return false; | |
| } | |
| Endpoint endpoint; | |
| if (nearestEndpoint(rootPosition, endpoint)) | |
| { | |
| m_extent = endpoint; | |
| updateRanges(); | |
| } | |
| return true; | |
| } | |
| bool TextSelectionController::pointerUp(Vec2D rootPosition, int pointerId) | |
| { | |
| if (!pointerMove(rootPosition, pointerId)) | |
| { | |
| return false; | |
| } | |
| m_dragging = false; | |
| return true; | |
| } | |
| bool TextSelectionController::pointerCancel(int pointerId) | |
| { | |
| if (!m_dragging || pointerId != m_pointerId) | |
| { | |
| return false; | |
| } | |
| clear(); | |
| return true; | |
| } | |
| bool TextSelectionController::select( | |
| Text* anchorText, | |
| uint32_t anchorOffset, | |
| Text* extentText, | |
| uint32_t extentOffset) | |
| { | |
| auto* anchor = find(anchorText); | |
| auto* extent = find(extentText); | |
| if (anchor == nullptr || extent == nullptr || !eligible(*anchor) || | |
| !eligible(*extent)) | |
| { | |
| return false; | |
| } | |
| m_dragging = false; | |
| m_anchor = {anchor, CursorPosition::atIndex(anchorOffset, layout(*anchor))}; | |
| m_externalRanges = false; | |
| m_extent = {extent, CursorPosition::atIndex(extentOffset, layout(*extent))}; | |
| updateRanges(); | |
| return true; | |
| } | |
| void TextSelectionController::selectAll() | |
| { | |
| Participant* first = nullptr; | |
| Participant* last = nullptr; | |
| for (auto& participant : m_participants) | |
| { | |
| if (eligible(*participant)) | |
| { | |
| if (first == nullptr) | |
| { | |
| first = participant.get(); | |
| } | |
| last = participant.get(); | |
| } | |
| } | |
| if (first != nullptr) | |
| { | |
| select(first->text, 0, last->text, (uint32_t)last->source.size()); | |
| } | |
| else | |
| { | |
| clear(); | |
| } | |
| } | |
| void TextSelectionController::updateRanges() | |
| { | |
| auto first = m_anchor; | |
| auto last = m_extent; | |
| size_t anchorIndex = 0, extentIndex = 0; | |
| for (size_t i = 0; i < m_participants.size(); i++) | |
| { | |
| if (m_participants[i].get() == first.participant) | |
| anchorIndex = i; | |
| if (m_participants[i].get() == last.participant) | |
| extentIndex = i; | |
| } | |
| if (anchorIndex > extentIndex || | |
| (anchorIndex == extentIndex && first.position > last.position)) | |
| { | |
| std::swap(first, last); | |
| std::swap(anchorIndex, extentIndex); | |
| } | |
| for (size_t i = 0; i < m_participants.size(); i++) | |
| { | |
| auto& participant = *m_participants[i]; | |
| uint32_t start = 0, end = 0; | |
| if (i >= anchorIndex && i <= extentIndex && eligible(participant)) | |
| { | |
| start = i == anchorIndex ? first.position.codePointIndex() : 0; | |
| end = i == extentIndex ? last.position.codePointIndex() | |
| : (uint32_t)participant.source.size(); | |
| } | |
| participant.cursor = Cursor(CursorPosition(start), CursorPosition(end)); | |
| participant.geometryDirty = true; | |
| } | |
| } | |
| bool TextSelectionController::hasSelection() const | |
| { | |
| if (!validSelection()) | |
| { | |
| return false; | |
| } | |
| if (m_externalRanges) | |
| { | |
| for (const auto& p : m_participants) | |
| if (p->cursor.hasSelection()) | |
| return true; | |
| return false; | |
| } | |
| return m_anchor.participant != m_extent.participant || | |
| m_anchor.position.codePointIndex() != | |
| m_extent.position.codePointIndex(); | |
| } | |
| Cursor TextSelectionController::selection(Text* text) const | |
| { | |
| auto* participant = find(text); | |
| return validSelection() && participant != nullptr ? participant->cursor | |
| : Cursor::zero(); | |
| } | |
| std::string TextSelectionController::selectedText() const | |
| { | |
| std::string result; | |
| if (!hasSelection()) | |
| { | |
| return result; | |
| } | |
| if (m_externalRanges) | |
| { | |
| for (const auto& p : m_participants) | |
| { | |
| if (!p->cursor.hasSelection()) | |
| continue; | |
| if (!result.empty()) | |
| result += p->separatorBefore; | |
| for (auto i = p->cursor.first().codePointIndex(); | |
| i < p->cursor.last().codePointIndex(); | |
| ++i) | |
| { | |
| uint8_t bytes[4]; | |
| auto count = UTF::Encode(bytes, p->source[i]); | |
| result.append(reinterpret_cast<char*>(bytes), count); | |
| } | |
| } | |
| return result; | |
| } | |
| bool started = false; | |
| bool inRange = false; | |
| uint32_t endpointsSeen = 0; | |
| // Include object boundaries even when an endpoint contributes zero | |
| // characters (end of A -> start of B still selects the separator). | |
| for (auto& participant : m_participants) | |
| { | |
| bool endpoint = participant.get() == m_anchor.participant || | |
| participant.get() == m_extent.participant; | |
| if (endpoint && !inRange) | |
| { | |
| inRange = true; | |
| } | |
| if (inRange && eligible(*participant)) | |
| { | |
| if (started) | |
| { | |
| result += participant->separatorBefore; | |
| } | |
| started = true; | |
| auto first = participant->cursor.first().codePointIndex(); | |
| auto last = participant->cursor.last().codePointIndex(); | |
| for (auto i = first; i < last; i++) | |
| { | |
| uint8_t bytes[4]; | |
| auto count = UTF::Encode(bytes, participant->source[i]); | |
| result.append(reinterpret_cast<char*>(bytes), count); | |
| } | |
| } | |
| if (endpoint) | |
| { | |
| endpointsSeen++; | |
| if (m_anchor.participant == m_extent.participant || endpointsSeen == 2) | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| Span<const AABB> TextSelectionController::selectionRects(Text* text) | |
| { | |
| auto* participant = find(text); | |
| if (participant == nullptr || !participant->cursor.hasSelection() || | |
| !validSelection()) | |
| { | |
| return {}; | |
| } | |
| float radius = std::max(0.0f, style().cornerRadius()); | |
| if (participant->geometryDirty || participant->radius != radius) | |
| { | |
| participant->rects.clear(); | |
| auto view = layout(*participant); | |
| if (!view.orderedLines().empty()) | |
| { | |
| // Range endpoints are source offsets. Walk every drawn line, so | |
| // hidden/trimmed lines do not confuse logical and visible indices. | |
| auto cursor = Cursor( | |
| CursorPosition(0, participant->cursor.first().codePointIndex()), | |
| CursorPosition( | |
| (uint32_t)view.orderedLines().size() - 1, | |
| participant->cursor.last().codePointIndex())); | |
| cursor.selectionRects(participant->rects, view); | |
| } | |
| participant->path.update(participant->rects, radius); | |
| participant->radius = radius; | |
| participant->geometryDirty = false; | |
| } | |
| return participant->rects; | |
| } | |
| void TextSelectionController::draw(Text* text, Renderer* renderer) | |
| { | |
| if (m_anchor.participant != nullptr && !validSelection()) | |
| { | |
| clear(); | |
| } | |
| if (selectionRects(text).empty()) | |
| { | |
| return; | |
| } | |
| auto* participant = find(text); | |
| auto* factory = text->artboard()->factory(); | |
| if (participant->paint == nullptr) | |
| { | |
| participant->paint = factory->makeRenderPaint(); | |
| participant->paint->style(RenderPaintStyle::fill); | |
| } | |
| participant->paint->color( | |
| colorModulateOpacity(style().highlightColor(), text->renderOpacity())); | |
| renderer->save(); | |
| renderer->transform(text->shapeWorldTransform()); | |
| renderer->drawPath( | |
| participant->path.renderPath(factory), | |
| participant->paint.get()); | |
| renderer->restore(); | |
| } | |
| const SelectionStyle& TextSelectionController::style() const | |
| { | |
| static const SelectionStyle defaults; | |
| const auto* fileStyle = m_styleFile == nullptr | |
| ? nullptr | |
| : m_styleFile->selectionStyle(); | |
| return m_style != nullptr ? *m_style | |
| : fileStyle != nullptr ? *fileStyle | |
| : defaults; | |
| } | |
| bool TextSelectionController::keyInput( | |
| Key key, | |
| KeyModifiers modifiers, | |
| bool isPressed) | |
| { | |
| if (!isPressed || !validSelection()) | |
| { | |
| return false; | |
| } | |
| if (key == Key::escape) | |
| { | |
| clear(); | |
| return true; | |
| } | |
| if (key == Key::a && (modifiers & (KeyModifiers::ctrl | | |
| KeyModifiers::meta)) != KeyModifiers::none) | |
| { | |
| selectAll(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| #endif |