2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
23 ==============================================================================
29 struct AccessibilityTextHelpers
31 /* Wraps a CharPtr into a stdlib-compatible iterator.
33 MSVC's std::reverse_iterator requires the wrapped iterator to be default constructible
34 when building in C++20 mode, but I don't really want to add public default constructors to
35 the CharPtr types. Instead, we add a very basic default constructor here which sets the
36 wrapped CharPtr to nullptr.
38 template <typename CharPtr
>
39 class CharPtrIteratorAdapter
42 using difference_type
= int;
43 using value_type
= decltype (*std::declval
<CharPtr
>());
44 using pointer
= value_type
*;
45 using reference
= value_type
;
46 using iterator_category
= std::bidirectional_iterator_tag
;
48 CharPtrIteratorAdapter() = default;
49 constexpr explicit CharPtrIteratorAdapter (CharPtr arg
) : ptr (arg
) {}
51 constexpr auto operator*() const { return *ptr
; }
53 constexpr CharPtrIteratorAdapter
& operator++()
59 constexpr CharPtrIteratorAdapter
& operator--()
65 constexpr bool operator== (const CharPtrIteratorAdapter
& other
) const { return ptr
== other
.ptr
; }
66 constexpr bool operator!= (const CharPtrIteratorAdapter
& other
) const { return ptr
!= other
.ptr
; }
68 constexpr auto operator+ (difference_type offset
) const { return CharPtrIteratorAdapter
{ ptr
+ offset
}; }
69 constexpr auto operator- (difference_type offset
) const { return CharPtrIteratorAdapter
{ ptr
- offset
}; }
75 template <typename CharPtr
>
76 static auto makeCharPtrIteratorAdapter (CharPtr ptr
)
78 return CharPtrIteratorAdapter
<CharPtr
> { ptr
};
81 enum class BoundaryType
95 enum class ExtendSelection
101 /* Indicates whether a function may return the current text position, in the case that the
102 position already falls on a text unit boundary.
104 enum class IncludeThisBoundary
106 no
, //< Always search for the following boundary, even if the current position falls on a boundary
107 yes
//< Return the current position if it falls on a boundary
110 /* Indicates whether a word boundary should include any whitespaces that follow the
111 non-whitespace characters.
113 enum class IncludeWhitespaceAfterWords
115 no
, //< The word ends on the first whitespace character
116 yes
//< The word ends after the last whitespace character
119 /* Like std::distance, but always does an O(N) count rather than an O(1) count, and doesn't
120 require the iterators to have any member type aliases.
122 template <typename Iter
>
123 static int countDifference (Iter from
, Iter to
)
136 /* Returns the number of characters between ptr and the next word end in a specific
139 If ptr is inside a word, the result will be the distance to the end of the same
142 template <typename CharPtr
>
143 static int findNextWordEndOffset (CharPtr beginIn
,
147 IncludeThisBoundary includeBoundary
,
148 IncludeWhitespaceAfterWords includeWhitespace
)
150 const auto begin
= makeCharPtrIteratorAdapter (beginIn
);
151 const auto end
= makeCharPtrIteratorAdapter (endIn
);
152 const auto ptr
= makeCharPtrIteratorAdapter (ptrIn
);
154 const auto move
= [&] (auto b
, auto e
, auto iter
)
156 const auto isSpace
= [] (juce_wchar c
) { return CharacterFunctions::isWhitespace (c
); };
158 const auto start
= [&]
160 if (iter
== b
&& includeBoundary
== IncludeThisBoundary::yes
)
163 const auto nudged
= iter
- (iter
!= b
&& includeBoundary
== IncludeThisBoundary::yes
? 1 : 0);
165 return includeWhitespace
== IncludeWhitespaceAfterWords::yes
166 ? std::find_if (nudged
, e
, isSpace
)
167 : std::find_if_not (nudged
, e
, isSpace
);
170 const auto found
= includeWhitespace
== IncludeWhitespaceAfterWords::yes
171 ? std::find_if_not (start
, e
, isSpace
)
172 : std::find_if (start
, e
, isSpace
);
174 return countDifference (iter
, found
);
177 return direction
== Direction::forwards
? move (begin
, end
, ptr
)
178 : -move (std::make_reverse_iterator (end
),
179 std::make_reverse_iterator (begin
),
180 std::make_reverse_iterator (ptr
));
183 /* Returns the number of characters between ptr and the beginning of the next line in a
186 template <typename CharPtr
>
187 static int findNextLineOffset (CharPtr beginIn
,
191 IncludeThisBoundary includeBoundary
)
193 const auto begin
= makeCharPtrIteratorAdapter (beginIn
);
194 const auto end
= makeCharPtrIteratorAdapter (endIn
);
195 const auto ptr
= makeCharPtrIteratorAdapter (ptrIn
);
197 const auto findNewline
= [] (auto from
, auto to
) { return std::find (from
, to
, juce_wchar
{ '\n' }); };
199 if (direction
== Direction::forwards
)
201 if (ptr
!= begin
&& includeBoundary
== IncludeThisBoundary::yes
&& *(ptr
- 1) == '\n')
204 const auto newline
= findNewline (ptr
, end
);
205 return countDifference (ptr
, newline
) + (newline
== end
? 0 : 1);
208 const auto rbegin
= std::make_reverse_iterator (ptr
);
209 const auto rend
= std::make_reverse_iterator (begin
);
211 return -countDifference (rbegin
, findNewline (rbegin
+ (rbegin
== rend
|| includeBoundary
== IncludeThisBoundary::yes
? 0 : 1), rend
));
214 /* Unfortunately, the method of computing end-points of text units depends on context, and on
215 the current platform.
217 Some examples of different behaviour:
218 - On Android, updating the cursor/selection always searches for the next text unit boundary;
219 but on Windows, ExpandToEnclosingUnit() should not move the starting point of the
220 selection if it already at a unit boundary. This means that we need both inclusive and
221 exclusive methods for finding the next text boundary.
222 - On Android, moving the cursor by 'words' should move to the first space following a
223 non-space character in the requested direction. On Windows, a 'word' includes trailing
224 whitespace, but not preceding whitespace. This means that we need a way of specifying
225 whether whitespace should be included when navigating by words.
227 static int findTextBoundary (const AccessibilityTextInterface
& textInterface
,
229 BoundaryType boundary
,
231 IncludeThisBoundary includeBoundary
,
232 IncludeWhitespaceAfterWords includeWhitespace
)
234 const auto numCharacters
= textInterface
.getTotalNumCharacters();
235 const auto isForwards
= (direction
== Direction::forwards
);
236 const auto currentClamped
= jlimit (0, numCharacters
, currentPosition
);
240 case BoundaryType::character
:
242 const auto offset
= includeBoundary
== IncludeThisBoundary::yes
? 0
243 : (isForwards
? 1 : -1);
244 return jlimit (0, numCharacters
, currentPosition
+ offset
);
247 case BoundaryType::word
:
249 const auto str
= textInterface
.getText ({ 0, numCharacters
});
250 return currentClamped
+ findNextWordEndOffset (str
.begin(),
252 str
.begin() + currentClamped
,
258 case BoundaryType::line
:
260 const auto str
= textInterface
.getText ({ 0, numCharacters
});
261 return currentClamped
+ findNextLineOffset (str
.begin(),
263 str
.begin() + currentClamped
,
268 case BoundaryType::document
:
269 return isForwards
? numCharacters
: 0;
276 /* Adjusts the current text selection range, using an algorithm appropriate for cursor movement
279 static Range
<int> findNewSelectionRangeAndroid (const AccessibilityTextInterface
& textInterface
,
280 BoundaryType boundaryType
,
281 ExtendSelection extend
,
284 const auto oldPos
= textInterface
.getTextInsertionOffset();
285 const auto cursorPos
= findTextBoundary (textInterface
,
289 IncludeThisBoundary::no
,
290 IncludeWhitespaceAfterWords::no
);
292 if (extend
== ExtendSelection::no
)
293 return { cursorPos
, cursorPos
};
295 const auto currentSelection
= textInterface
.getSelection();
296 const auto start
= currentSelection
.getStart();
297 const auto end
= currentSelection
.getEnd();
298 return Range
<int>::between (cursorPos
, oldPos
== start
? end
: start
);