| author | |
| committer | |
| log | d8a3bb046d2bdf597b51e08a041375538e1aad16 |
| tree | 0160a5960e8351b2ae3cf7cb185565ae7dc3a0b7 |
| parent | 10cde9f3e120e69e22c88fe33ecfe264bd350fb0 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
The page takes the system keyboard. Its input connection speaks the active outline's text in
UTF-16 offsets, the unit the page measures in: typed and composed words go to the editor as
iOS sends them, the keyboard's Backspace deletes as the Delete key does, and Enter, Tab and
the arrow keys arrive as keys, from a hardware keyboard too. A tap or Tab that moves to
another outline has the keyboard read it afresh.
The caret and selection are drawn over the page in the system's accent colour, and the
page shrinks above the keyboard. The keyboard can't yet reopen a word already typed for
correction, as the page has no way to mark existing text.
Refs #24
Assisted-by: claude-opus-5.56 files changed, 376 insertions(+), 2 deletions(-)
apps/android/AndroidManifest.xml+1-1| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | android:versionCode="1" android:versionName="0.1"> |
| 3 | 3 | <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36" /> |
| 4 | 4 | <application android:label="Snowbound" android:theme="@style/Page" android:extractNativeLibs="false"> |
| 5 | <activity android:name=".PageActivity" android:exported="true"> | |
| 5 | <activity android:name=".PageActivity" android:exported="true" android:windowSoftInputMode="adjustResize"> | |
| 6 | 6 | <intent-filter> |
| 7 | 7 | <action android:name="android.intent.action.MAIN" /> |
| 8 | 8 | <category android:name="android.intent.category.LAUNCHER" /> |
apps/android/jni/snowbound.c+86| ... | ... | @@ -212,3 +212,89 @@ JNIEXPORT void JNICALL JNI(release)(JNIEnv *env, jclass cls, jlong view) { |
| 212 | 212 | (void)env, (void)cls; |
| 213 | 213 | sb_view_release((View *)(intptr_t)view); |
| 214 | 214 | } |
| 215 | ||
| 216 | JNIEXPORT jint JNICALL JNI(length)(JNIEnv *env, jclass cls, jlong view) { | |
| 217 | (void)env, (void)cls; | |
| 218 | return (jint)sb_text_length((const View *)(intptr_t)view); | |
| 219 | } | |
| 220 | ||
| 221 | JNIEXPORT jstring JNICALL JNI(text)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) { | |
| 222 | (void)cls; | |
| 223 | return output(env, sb_text((const View *)(intptr_t)view, (uint32_t)start, (uint32_t)end)); | |
| 224 | } | |
| 225 | ||
| 226 | static jintArray range(JNIEnv *env, const uint32_t range[2]) { | |
| 227 | jint values[2] = {(jint)range[0], (jint)range[1]}; | |
| 228 | jintArray result = (*env)->NewIntArray(env, 2); | |
| 229 | if (result) (*env)->SetIntArrayRegion(env, result, 0, 2, values); | |
| 230 | return result; | |
| 231 | } | |
| 232 | ||
| 233 | JNIEXPORT jintArray JNICALL JNI(selection)(JNIEnv *env, jclass cls, jlong view) { | |
| 234 | (void)cls; | |
| 235 | uint32_t selected[2] = {0, 0}; | |
| 236 | sb_selection((const View *)(intptr_t)view, selected); | |
| 237 | return range(env, selected); | |
| 238 | } | |
| 239 | ||
| 240 | JNIEXPORT jintArray JNICALL JNI(marked)(JNIEnv *env, jclass cls, jlong view) { | |
| 241 | (void)cls; | |
| 242 | uint32_t marked[2]; | |
| 243 | return sb_marked((const View *)(intptr_t)view, marked) ? range(env, marked) : NULL; | |
| 244 | } | |
| 245 | ||
| 246 | JNIEXPORT jboolean JNICALL JNI(select)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) { | |
| 247 | (void)env, (void)cls; | |
| 248 | return sb_select((View *)(intptr_t)view, (uint32_t)start, (uint32_t)end); | |
| 249 | } | |
| 250 | ||
| 251 | JNIEXPORT jboolean JNICALL JNI(insert)(JNIEnv *env, jclass cls, jlong view, jstring text) { | |
| 252 | (void)cls; | |
| 253 | char *t = utf8(env, text); | |
| 254 | bool changed = t && sb_insert((View *)(intptr_t)view, t); | |
| 255 | free(t); | |
| 256 | return changed; | |
| 257 | } | |
| 258 | ||
| 259 | JNIEXPORT jboolean JNICALL JNI(setMarked)(JNIEnv *env, jclass cls, jlong view, jstring text, jint start, jint end) { | |
| 260 | (void)cls; | |
| 261 | char *t = utf8(env, text); | |
| 262 | bool changed = t && sb_set_marked((View *)(intptr_t)view, t, (uint32_t)start, (uint32_t)end); | |
| 263 | free(t); | |
| 264 | return changed; | |
| 265 | } | |
| 266 | ||
| 267 | JNIEXPORT void JNICALL JNI(unmark)(JNIEnv *env, jclass cls, jlong view) { | |
| 268 | (void)env, (void)cls; | |
| 269 | sb_unmark((View *)(intptr_t)view); | |
| 270 | } | |
| 271 | ||
| 272 | JNIEXPORT jboolean JNICALL JNI(deleteBackward)(JNIEnv *env, jclass cls, jlong view) { | |
| 273 | (void)env, (void)cls; | |
| 274 | return sb_delete_backward((View *)(intptr_t)view); | |
| 275 | } | |
| 276 | ||
| 277 | // The caret at `start` when `end` is the same, else the rectangles covering the range: each | |
| 278 | // `x, y, width, height` in the view's points. | |
| 279 | JNIEXPORT jfloatArray JNICALL JNI(rects)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) { | |
| 280 | (void)cls; | |
| 281 | const View *v = (const View *)(intptr_t)view; | |
| 282 | float caret[1][4]; | |
| 283 | size_t count = start == end ? sb_caret_rect(v, (uint32_t)start, caret[0]) | |
| 284 | : sb_range_rects(v, (uint32_t)start, (uint32_t)end, NULL, 0); | |
| 285 | float (*rects)[4] = start == end ? caret : malloc(sizeof(*rects) * (count ? count : 1)); | |
| 286 | if (!rects) return NULL; | |
| 287 | if (start != end) { | |
| 288 | size_t written = sb_range_rects(v, (uint32_t)start, (uint32_t)end, rects, count); | |
| 289 | if (written < count) count = written; | |
| 290 | } | |
| 291 | jfloatArray result = (*env)->NewFloatArray(env, (jsize)(count * 4)); | |
| 292 | if (result) (*env)->SetFloatArrayRegion(env, result, 0, (jsize)(count * 4), (const jfloat *)rects); | |
| 293 | if (rects != caret) free(rects); | |
| 294 | return result; | |
| 295 | } | |
| 296 | ||
| 297 | JNIEXPORT jint JNICALL JNI(closest)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) { | |
| 298 | (void)env, (void)cls; | |
| 299 | return (jint)sb_closest((const View *)(intptr_t)view, x, y); | |
| 300 | } |
apps/android/src/net/paperclover/snowbound/Editor.java+11| ... | ... | @@ -82,4 +82,15 @@ final class Editor { |
| 82 | 82 | static native void press(long view, float x, float y); |
| 83 | 83 | static native void drag(long view, float x, float y); |
| 84 | 84 | static native void release(long view); |
| 85 | static native int length(long view); | |
| 86 | static native String text(long view, int start, int end); | |
| 87 | static native int[] selection(long view); | |
| 88 | static native int[] marked(long view); | |
| 89 | static native boolean select(long view, int start, int end); | |
| 90 | static native boolean insert(long view, String text); | |
| 91 | static native boolean setMarked(long view, String text, int start, int end); | |
| 92 | static native void unmark(long view); | |
| 93 | static native boolean deleteBackward(long view); | |
| 94 | static native float[] rects(long view, int start, int end); | |
| 95 | static native int closest(long view, float x, float y); | |
| 85 | 96 | } |
apps/android/src/net/paperclover/snowbound/PageActivity.java+1| ... | ... | @@ -14,6 +14,7 @@ public final class PageActivity extends Activity { |
| 14 | 14 | FrameLayout frame = new FrameLayout(this); |
| 15 | 15 | frame.setFitsSystemWindows(true); |
| 16 | 16 | frame.addView(page); |
| 17 | frame.addView(page.caret); | |
| 17 | 18 | setContentView(frame); |
| 18 | 19 | Editor.shown = page; |
| 19 | 20 | Editor.open(this); |
apps/android/src/net/paperclover/snowbound/PageInput.java created+129| ... | ... | @@ -0,0 +1,129 @@ |
| 1 | package net.paperclover.snowbound; | |
| 2 | ||
| 3 | import android.text.TextUtils; | |
| 4 | import android.view.inputmethod.BaseInputConnection; | |
| 5 | ||
| 6 | /** The keyboard's view of the page: the active outline's text, in UTF-16 offsets. */ | |
| 7 | final class PageInput extends BaseInputConnection { | |
| 8 | private final PageView page; | |
| 9 | /** Nested batch edits, until whose end the keyboard hears of no change. */ | |
| 10 | int batch; | |
| 11 | ||
| 12 | PageInput(PageView page) { | |
| 13 | super(page, false); | |
| 14 | this.page = page; | |
| 15 | } | |
| 16 | ||
| 17 | private static int clamp(int offset) { | |
| 18 | return Math.max(0, Math.min(offset, Editor.length(Editor.view))); | |
| 19 | } | |
| 20 | ||
| 21 | private boolean edited() { | |
| 22 | page.edited(); | |
| 23 | return true; | |
| 24 | } | |
| 25 | ||
| 26 | @Override | |
| 27 | public boolean beginBatchEdit() { | |
| 28 | batch++; | |
| 29 | return true; | |
| 30 | } | |
| 31 | ||
| 32 | @Override | |
| 33 | public boolean endBatchEdit() { | |
| 34 | if (batch > 0 && --batch == 0) page.edited(); | |
| 35 | return batch > 0; | |
| 36 | } | |
| 37 | ||
| 38 | @Override | |
| 39 | public boolean commitText(CharSequence text, int position) { | |
| 40 | long view = Editor.view; | |
| 41 | String value = text.toString(); | |
| 42 | int[] marked = Editor.marked(view); | |
| 43 | int start = marked != null ? marked[0] : Editor.selection(view)[0]; | |
| 44 | Editor.insert(view, value); | |
| 45 | if (position != 1) { | |
| 46 | int cursor = clamp(position > 0 ? start + value.length() + position - 1 : start + position); | |
| 47 | Editor.select(view, cursor, cursor); | |
| 48 | } | |
| 49 | return edited(); | |
| 50 | } | |
| 51 | ||
| 52 | @Override | |
| 53 | public boolean setComposingText(CharSequence text, int position) { | |
| 54 | String value = text.toString(); | |
| 55 | int cursor = Math.max(0, Math.min(position > 0 ? value.length() + position - 1 : position, value.length())); | |
| 56 | Editor.setMarked(Editor.view, value, cursor, cursor); | |
| 57 | return edited(); | |
| 58 | } | |
| 59 | ||
| 60 | /** The page can't mark text already typed, so the keyboard composes only new text. */ | |
| 61 | @Override | |
| 62 | public boolean setComposingRegion(int start, int end) { | |
| 63 | return false; | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | public boolean finishComposingText() { | |
| 68 | Editor.unmark(Editor.view); | |
| 69 | return edited(); | |
| 70 | } | |
| 71 | ||
| 72 | @Override | |
| 73 | public boolean setSelection(int start, int end) { | |
| 74 | Editor.select(Editor.view, clamp(Math.min(start, end)), clamp(Math.max(start, end))); | |
| 75 | return edited(); | |
| 76 | } | |
| 77 | ||
| 78 | @Override | |
| 79 | public boolean deleteSurroundingText(int before, int after) { | |
| 80 | long view = Editor.view; | |
| 81 | int[] selected = Editor.selection(view); | |
| 82 | int start = clamp(selected[0] - before), end = clamp(selected[1] + after); | |
| 83 | if (end > selected[1]) { | |
| 84 | Editor.select(view, selected[1], end); | |
| 85 | Editor.deleteBackward(view); | |
| 86 | } | |
| 87 | if (start < selected[0]) { | |
| 88 | // Backspace itself deletes a whole character at the caret, as the keyboard asks. | |
| 89 | if (selected[0] != selected[1] || before != 1) Editor.select(view, start, selected[0]); | |
| 90 | Editor.deleteBackward(view); | |
| 91 | } | |
| 92 | if (selected[0] != selected[1]) Editor.select(view, start, start + selected[1] - selected[0]); | |
| 93 | return edited(); | |
| 94 | } | |
| 95 | ||
| 96 | @Override | |
| 97 | public CharSequence getTextBeforeCursor(int count, int flags) { | |
| 98 | long view = Editor.view; | |
| 99 | int end = Editor.selection(view)[0], start = Math.max(0, end - count); | |
| 100 | // One unit more, so that a surrogate pair cut at the start is dropped whole. | |
| 101 | String text = Editor.text(view, Math.max(0, start - 1), end); | |
| 102 | if (start == 0) return text; | |
| 103 | boolean pair = text.length() > 1 && Character.isSurrogatePair(text.charAt(0), text.charAt(1)); | |
| 104 | return text.substring(pair ? 2 : 1); | |
| 105 | } | |
| 106 | ||
| 107 | @Override | |
| 108 | public CharSequence getTextAfterCursor(int count, int flags) { | |
| 109 | long view = Editor.view; | |
| 110 | int start = Editor.selection(view)[1]; | |
| 111 | String text = Editor.text(view, start, clamp(start + Math.max(0, count) + 1)); | |
| 112 | if (text.length() <= count) return text; | |
| 113 | if (count <= 0) return ""; | |
| 114 | boolean pair = Character.isSurrogatePair(text.charAt(count - 1), text.charAt(count)); | |
| 115 | return text.substring(0, pair ? count - 1 : count); | |
| 116 | } | |
| 117 | ||
| 118 | @Override | |
| 119 | public CharSequence getSelectedText(int flags) { | |
| 120 | int[] selected = Editor.selection(Editor.view); | |
| 121 | return selected[0] == selected[1] ? null : Editor.text(Editor.view, selected[0], selected[1]); | |
| 122 | } | |
| 123 | ||
| 124 | @Override | |
| 125 | public int getCursorCapsMode(int modes) { | |
| 126 | CharSequence before = getTextBeforeCursor(64, 0); | |
| 127 | return TextUtils.getCapsMode(before, before.length(), modes); | |
| 128 | } | |
| 129 | } |
apps/android/src/net/paperclover/snowbound/PageView.java+148-1| ... | ... | @@ -1,20 +1,36 @@ |
| 1 | 1 | package net.paperclover.snowbound; |
| 2 | 2 | |
| 3 | 3 | import android.content.Context; |
| 4 | import android.content.res.TypedArray; | |
| 5 | import android.graphics.Canvas; | |
| 6 | import android.graphics.Paint; | |
| 4 | 7 | import android.graphics.Rect; |
| 8 | import android.text.InputType; | |
| 5 | 9 | import android.view.Choreographer; |
| 10 | import android.view.KeyCharacterMap; | |
| 11 | import android.view.KeyEvent; | |
| 6 | 12 | import android.view.MotionEvent; |
| 7 | 13 | import android.view.SurfaceHolder; |
| 8 | 14 | import android.view.SurfaceView; |
| 15 | import android.view.View; | |
| 16 | import android.view.inputmethod.EditorInfo; | |
| 17 | import android.view.inputmethod.InputConnection; | |
| 18 | import android.view.inputmethod.InputMethodManager; | |
| 9 | 19 | |
| 10 | /** Draws the editor's page into its surface, a frame whenever the page changes. */ | |
| 20 | /** Draws the editor's page into its surface, a frame whenever the page changes, and takes its typing. */ | |
| 11 | 21 | final class PageView extends SurfaceView implements SurfaceHolder.Callback, Choreographer.FrameCallback { |
| 22 | /** The caret and selection, drawn over the page by the view above it. */ | |
| 23 | final View caret; | |
| 24 | private final InputMethodManager keyboard; | |
| 25 | private PageInput input; | |
| 12 | 26 | private boolean attached, dirty; |
| 13 | 27 | |
| 14 | 28 | PageView(Context context) { |
| 15 | 29 | super(context); |
| 16 | 30 | setFocusableInTouchMode(true); |
| 17 | 31 | getHolder().addCallback(this); |
| 32 | caret = new Caret(context); | |
| 33 | keyboard = context.getSystemService(InputMethodManager.class); | |
| 18 | 34 | } |
| 19 | 35 | |
| 20 | 36 | private float scale() { |
| ... | ... | @@ -43,6 +59,7 @@ final class PageView extends SurfaceView implements SurfaceHolder.Callback, Chor |
| 43 | 59 | float scale = scale(); |
| 44 | 60 | Editor.resize(Editor.view, width / scale, height / scale, scale); |
| 45 | 61 | dirty = true; |
| 62 | caret.invalidate(); | |
| 46 | 63 | } |
| 47 | 64 | |
| 48 | 65 | /** Android destroys the surface once this returns, so the editor lets go of it first. */ |
| ... | ... | @@ -63,6 +80,22 @@ final class PageView extends SurfaceView implements SurfaceHolder.Callback, Chor |
| 63 | 80 | Editor.render(Editor.view); |
| 64 | 81 | } |
| 65 | 82 | |
| 83 | /** Draws the change and tells the keyboard where the selection and composition are now. */ | |
| 84 | void edited() { | |
| 85 | dirty = true; | |
| 86 | caret.invalidate(); | |
| 87 | if (input == null || input.batch > 0) return; | |
| 88 | int[] selected = Editor.selection(Editor.view), marked = Editor.marked(Editor.view); | |
| 89 | keyboard.updateSelection(this, selected[0], selected[1], marked == null ? -1 : marked[0], | |
| 90 | marked == null ? -1 : marked[1]); | |
| 91 | } | |
| 92 | ||
| 93 | /** The keyboard reads the active outline afresh, as a tap or Tab may have changed it. */ | |
| 94 | private void restartInput() { | |
| 95 | keyboard.restartInput(this); | |
| 96 | edited(); | |
| 97 | } | |
| 98 | ||
| 66 | 99 | @Override |
| 67 | 100 | public boolean onTouchEvent(MotionEvent event) { |
| 68 | 101 | if (!attached) return false; |
| ... | ... | @@ -78,14 +111,98 @@ final class PageView extends SurfaceView implements SurfaceHolder.Callback, Chor |
| 78 | 111 | case MotionEvent.ACTION_UP: |
| 79 | 112 | case MotionEvent.ACTION_CANCEL: |
| 80 | 113 | Editor.release(Editor.view); |
| 114 | restartInput(); | |
| 115 | keyboard.showSoftInput(this, 0); | |
| 81 | 116 | break; |
| 82 | 117 | default: |
| 83 | 118 | return false; |
| 84 | 119 | } |
| 85 | 120 | dirty = true; |
| 121 | caret.invalidate(); | |
| 122 | return true; | |
| 123 | } | |
| 124 | ||
| 125 | @Override | |
| 126 | public boolean onCheckIsTextEditor() { | |
| 127 | return true; | |
| 128 | } | |
| 129 | ||
| 130 | @Override | |
| 131 | public InputConnection onCreateInputConnection(EditorInfo info) { | |
| 132 | if (!attached) return null; | |
| 133 | info.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | |
| 134 | | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; | |
| 135 | info.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN; | |
| 136 | int[] selected = Editor.selection(Editor.view); | |
| 137 | info.initialSelStart = selected[0]; | |
| 138 | info.initialSelEnd = selected[1]; | |
| 139 | return input = new PageInput(this); | |
| 140 | } | |
| 141 | ||
| 142 | /** Keys the keyboard sends rather than types, and those of a hardware keyboard. */ | |
| 143 | @Override | |
| 144 | public boolean onKeyDown(int code, KeyEvent event) { | |
| 145 | if (!attached || event.isCtrlPressed() || event.isAltPressed() || event.isMetaPressed()) { | |
| 146 | return super.onKeyDown(code, event); | |
| 147 | } | |
| 148 | long view = Editor.view; | |
| 149 | switch (code) { | |
| 150 | case KeyEvent.KEYCODE_DEL: | |
| 151 | Editor.deleteBackward(view); | |
| 152 | break; | |
| 153 | case KeyEvent.KEYCODE_ENTER: | |
| 154 | case KeyEvent.KEYCODE_NUMPAD_ENTER: | |
| 155 | Editor.insert(view, "\n"); | |
| 156 | break; | |
| 157 | case KeyEvent.KEYCODE_TAB: | |
| 158 | Editor.insert(view, "\t"); | |
| 159 | restartInput(); | |
| 160 | return true; | |
| 161 | case KeyEvent.KEYCODE_DPAD_LEFT: | |
| 162 | case KeyEvent.KEYCODE_DPAD_RIGHT: | |
| 163 | across(code == KeyEvent.KEYCODE_DPAD_RIGHT); | |
| 164 | break; | |
| 165 | case KeyEvent.KEYCODE_DPAD_UP: | |
| 166 | case KeyEvent.KEYCODE_DPAD_DOWN: | |
| 167 | upOrDown(code == KeyEvent.KEYCODE_DPAD_DOWN); | |
| 168 | break; | |
| 169 | default: | |
| 170 | int character = event.getUnicodeChar(); | |
| 171 | if (character == 0 || (character & KeyCharacterMap.COMBINING_ACCENT) != 0) { | |
| 172 | return super.onKeyDown(code, event); | |
| 173 | } | |
| 174 | Editor.insert(view, new String(Character.toChars(character))); | |
| 175 | } | |
| 176 | edited(); | |
| 86 | 177 | return true; |
| 87 | 178 | } |
| 88 | 179 | |
| 180 | /** Collapses a selection to its side, or moves the caret a character, a surrogate pair whole. */ | |
| 181 | private static void across(boolean right) { | |
| 182 | long view = Editor.view; | |
| 183 | int[] selected = Editor.selection(view); | |
| 184 | int offset = right ? selected[1] : selected[0]; | |
| 185 | if (selected[0] == selected[1]) { | |
| 186 | offset = Math.max(0, Math.min(offset + (right ? 1 : -1), Editor.length(view))); | |
| 187 | String around = offset > 0 ? Editor.text(view, offset - 1, offset + 1) : ""; | |
| 188 | if (around.length() == 2 && Character.isSurrogatePair(around.charAt(0), around.charAt(1))) { | |
| 189 | offset += right ? 1 : -1; | |
| 190 | } | |
| 191 | } | |
| 192 | Editor.select(view, offset, offset); | |
| 193 | } | |
| 194 | ||
| 195 | /** Moves the caret to the nearest place a line above or below. */ | |
| 196 | private static void upOrDown(boolean down) { | |
| 197 | long view = Editor.view; | |
| 198 | int[] selected = Editor.selection(view); | |
| 199 | float[] caret = Editor.rects(view, down ? selected[1] : selected[0], down ? selected[1] : selected[0]); | |
| 200 | if (caret == null || caret.length < 4) return; | |
| 201 | float y = caret[1] + caret[3] / 2 + (down ? caret[3] : -caret[3]); | |
| 202 | int offset = Editor.closest(view, caret[0], y); | |
| 203 | Editor.select(view, offset, offset); | |
| 204 | } | |
| 205 | ||
| 89 | 206 | @Override |
| 90 | 207 | protected void onFocusChanged(boolean focused, int direction, Rect previous) { |
| 91 | 208 | super.onFocusChanged(focused, direction, previous); |
| ... | ... | @@ -102,5 +219,35 @@ final class PageView extends SurfaceView implements SurfaceHolder.Callback, Chor |
| 102 | 219 | if (!attached) return; |
| 103 | 220 | Editor.focus(Editor.view, hasFocus() && hasWindowFocus()); |
| 104 | 221 | dirty = true; |
| 222 | caret.invalidate(); | |
| 223 | } | |
| 224 | ||
| 225 | /** The caret, a line in the system's accent colour, or the selection tinted with it. */ | |
| 226 | private final class Caret extends View { | |
| 227 | private final Paint paint = new Paint(); | |
| 228 | private final int accent; | |
| 229 | ||
| 230 | Caret(Context context) { | |
| 231 | super(context); | |
| 232 | TypedArray theme = context.obtainStyledAttributes(new int[] {android.R.attr.colorAccent}); | |
| 233 | accent = theme.getColor(0, 0xff000000); | |
| 234 | theme.recycle(); | |
| 235 | } | |
| 236 | ||
| 237 | @Override | |
| 238 | protected void onDraw(Canvas canvas) { | |
| 239 | if (!attached || !PageView.this.hasFocus()) return; | |
| 240 | int[] selected = Editor.selection(Editor.view); | |
| 241 | float[] rects = Editor.rects(Editor.view, selected[0], selected[1]); | |
| 242 | if (rects == null) return; | |
| 243 | float scale = scale(); | |
| 244 | boolean collapsed = selected[0] == selected[1]; | |
| 245 | paint.setColor(collapsed ? accent : accent & 0x00ffffff | 0x40000000); | |
| 246 | for (int i = 0; i + 3 < rects.length; i += 4) { | |
| 247 | float left = rects[i] * scale, top = rects[i + 1] * scale, bottom = (rects[i + 1] + rects[i + 3]) * scale; | |
| 248 | float right = collapsed ? left + scale : (rects[i] + rects[i + 2]) * scale; | |
| 249 | canvas.drawRect(collapsed ? left - scale : left, top, right, bottom, paint); | |
| 250 | } | |
| 251 | } | |
| 105 | 252 | } |
| 106 | 253 | } |