Dreamine.UI.Maui 1.0.1
Windows(WinUI)의 네이티브 `CheckBox`는 `WidthRequest`로 줄일 수 없는 거대한 최소 너비를 가져서 라벨과의 간격이 벌어지는 문제가 있어, 처음부터 직접 그려서 만들었습니다.
로딩중...
검색중...
일치하는것 없음
DreamineVirtualKeyboard.xaml.cs
이 파일의 문서화 페이지로 가기
1namespace Dreamine.UI.Maui;
2
19public partial class DreamineVirtualKeyboard : ContentView
20{
29 private static readonly Color KeyBackground = Color.FromArgb("#F1685E");
38 private static readonly Color KeySelectedBackground = Color.FromArgb("#F59584");
39
48 private readonly List<KeyButton> _textKeys = [];
57 private readonly HangulComposer _composer = new();
66 private Entry? _target;
67
76 private bool _shift;
85 private bool _capsLock;
94 private bool _korean;
103 private Button? _shiftButton;
112 private Button? _capsButton;
121 private Button? _languageButton;
122
132 {
133 InitializeComponent();
135 RefreshKeys();
136 }
137
162 public void Attach(Entry entry)
163 {
164 _target = entry;
165 entry.Focused += (_, _) => IsVisible = true;
166 }
167
176 private void BuildKeyboard()
177 {
178 RowsHost.Children.Clear();
179
180 AddRow(
181 [
182 KeySpec.Command("Esc", 54, HideKeyboard),
183 KeySpec.Text("`"), KeySpec.Text("1"), KeySpec.Text("2"), KeySpec.Text("3"), KeySpec.Text("4"),
184 KeySpec.Text("5"), KeySpec.Text("6"), KeySpec.Text("7"), KeySpec.Text("8"), KeySpec.Text("9"),
185 KeySpec.Text("0"), KeySpec.Text("-"), KeySpec.Text("="),
186 KeySpec.Command("Backspace", 116, Backspace)
187 ]);
188
189 AddRow(
190 [
191 KeySpec.Command("Tab", 100, () => InsertRaw(" ")),
192 KeySpec.Text("q"), KeySpec.Text("w"), KeySpec.Text("e"), KeySpec.Text("r"), KeySpec.Text("t"),
193 KeySpec.Text("y"), KeySpec.Text("u"), KeySpec.Text("i"), KeySpec.Text("o"), KeySpec.Text("p"),
194 KeySpec.Text("["), KeySpec.Text("]"), KeySpec.Text("\\", 100)
195 ]);
196
197 AddRow(
198 [
199 KeySpec.Command("Caps Lock", 116, ToggleCapsLock),
200 KeySpec.Text("a"), KeySpec.Text("s"), KeySpec.Text("d"), KeySpec.Text("f"), KeySpec.Text("g"),
201 KeySpec.Text("h"), KeySpec.Text("j"), KeySpec.Text("k"), KeySpec.Text("l"),
202 KeySpec.Text(";"), KeySpec.Text("'"),
203 KeySpec.Command("Enter", 116, OnEnter)
204 ]);
205
206 AddRow(
207 [
208 KeySpec.Command("Shift", 138, ToggleShift),
209 KeySpec.Text("z"), KeySpec.Text("x"), KeySpec.Text("c"), KeySpec.Text("v"), KeySpec.Text("b"),
210 KeySpec.Text("n"), KeySpec.Text("m"), KeySpec.Text(","), KeySpec.Text("."), KeySpec.Text("/"),
211 KeySpec.Command("◀", 58, MoveLeft),
212 KeySpec.Command("▶", 58, MoveRight)
213 ]);
214
215 AddRow(
216 [
217 KeySpec.Command("Ctrl", 124, () => { }),
218 KeySpec.Command("Space", 696, () => InsertRaw(" ")),
219 KeySpec.Command("abc", 72, ToggleLanguage)
220 ]);
221 }
222
239 private void AddRow(IReadOnlyList<KeySpec> specs)
240 {
241 var row = new Grid
242 {
243 ColumnSpacing = 4,
244 HeightRequest = 54,
245 HorizontalOptions = LayoutOptions.Fill
246 };
247
248 for (var i = 0; i < specs.Count; i++)
249 {
250 var width = specs[i].Width;
251 row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(width, GridUnitType.Star) });
252 var button = MakeKey(specs[i]);
253 row.Add(button, i, 0);
254 }
255
256 RowsHost.Children.Add(row);
257 }
258
283 private Button MakeKey(KeySpec spec)
284 {
285 var button = new Button
286 {
287 Text = spec.Label,
288 HeightRequest = 54,
289 Padding = 0,
290 FontSize = spec.Width > 90 ? 14 : 16,
291 BackgroundColor = KeyBackground,
292 TextColor = Colors.White,
293 CornerRadius = 4
294 };
295
296 if (spec.Kind == KeyKind.Text)
297 _textKeys.Add(new KeyButton(button, spec.Label));
298
299 if (spec.Label == "Shift")
300 _shiftButton = button;
301 else if (spec.Label == "Caps Lock")
302 _capsButton = button;
303 else if (spec.Label is "abc" or "가")
304 _languageButton = button;
305
306 button.Clicked += (_, _) =>
307 {
308 if (_target is null)
309 return;
310
311 if (spec.Kind == KeyKind.Text)
312 InsertKey(spec.Label);
313 else
314 spec.Action?.Invoke();
315 };
316
317 return button;
318 }
319
336 private void InsertKey(string key)
337 {
338 var text = GetKeyText(key);
339 if (_korean && HangulComposer.IsComposableJamo(text))
340 {
341 var edit = _composer.Input(text, GetTextBeforeCaret());
342 ReplaceTextTail(edit.ReplaceCount, edit.Text);
343 }
344 else
345 {
346 _composer.Reset();
347 InsertRaw(text);
348 }
349
350 if (_shift)
351 {
352 _shift = false;
353 RefreshKeys();
354 }
355 }
356
373 private void InsertRaw(string text)
374 {
375 if (_target is null)
376 return;
377
378 _composer.Reset();
379 ReplaceTextTail(0, text);
380 }
381
406 private void ReplaceTextTail(int replaceCount, string text)
407 {
408 if (_target is null)
409 return;
410
411 var current = _target.Text ?? string.Empty;
412 var selectionStart = Math.Clamp(_target.CursorPosition, 0, current.Length);
413 var selectionLength = Math.Clamp(_target.SelectionLength, 0, current.Length - selectionStart);
414
415 if (selectionLength > 0)
416 {
417 _target.Text = current.Remove(selectionStart, selectionLength).Insert(selectionStart, text);
418 _target.CursorPosition = selectionStart + text.Length;
419 }
420 else
421 {
422 var removeStart = Math.Max(0, selectionStart - replaceCount);
423 removeStart = Math.Min(removeStart, current.Length);
424 var removeLength = Math.Clamp(selectionStart - removeStart, 0, current.Length - removeStart);
425 _target.Text = current.Remove(removeStart, removeLength).Insert(removeStart, text);
426 _target.CursorPosition = removeStart + text.Length;
427 }
428 }
429
446 private string GetTextBeforeCaret()
447 {
448 if (_target is null)
449 return string.Empty;
450
451 var text = _target.Text ?? string.Empty;
452 var caret = Math.Clamp(_target.CursorPosition, 0, text.Length);
453 return text[..caret];
454 }
455
464 private void Backspace()
465 {
466 if (_target is null)
467 return;
468
469 _composer.Reset();
470 var current = _target.Text ?? string.Empty;
471 var selectionStart = Math.Clamp(_target.CursorPosition, 0, current.Length);
472 var selectionLength = Math.Clamp(_target.SelectionLength, 0, current.Length - selectionStart);
473
474 if (selectionLength > 0)
475 {
476 _target.Text = current.Remove(selectionStart, selectionLength);
477 _target.CursorPosition = selectionStart;
478 }
479 else if (selectionStart > 0)
480 {
481 _target.Text = current.Remove(selectionStart - 1, 1);
482 _target.CursorPosition = selectionStart - 1;
483 }
484 }
485
494 private void ToggleShift()
495 {
496 _shift = !_shift;
497 RefreshKeys();
498 }
499
508 private void ToggleCapsLock()
509 {
511 _composer.Reset();
512 RefreshKeys();
513 }
514
523 private void ToggleLanguage()
524 {
525 _korean = !_korean;
526 _composer.Reset();
527 RefreshKeys();
528 }
529
538 private void MoveLeft()
539 {
540 if (_target is null)
541 return;
542
543 _target.CursorPosition = Math.Max(0, _target.CursorPosition - 1);
544 _target.SelectionLength = 0;
545 }
546
555 private void MoveRight()
556 {
557 if (_target is null)
558 return;
559
560 _target.CursorPosition = Math.Min((_target.Text ?? string.Empty).Length, _target.CursorPosition + 1);
561 _target.SelectionLength = 0;
562 }
563
572 private void HideKeyboard()
573 {
574 _composer.Reset();
575 IsVisible = false;
576 }
577
586 private void OnEnter()
587 {
588 _composer.Reset();
589 _target?.Unfocus();
590 IsVisible = false;
591 }
592
601 private void RefreshKeys()
602 {
603 foreach (var key in _textKeys)
604 key.Button.Text = GetKeyText(key.BaseText);
605
606 if (_shiftButton is not null)
607 _shiftButton.BackgroundColor = _shift ? KeySelectedBackground : KeyBackground;
608
609 if (_capsButton is not null)
610 _capsButton.BackgroundColor = _capsLock ? KeySelectedBackground : KeyBackground;
611
612 if (_languageButton is not null)
613 {
614 _languageButton.Text = _korean ? "가" : "abc";
615 _languageButton.BackgroundColor = _korean ? KeySelectedBackground : KeyBackground;
616 }
617 }
618
651 private string GetKeyText(string key)
652 {
653 if (_korean && KoreanKeys.TryGetValue(key, out var korean))
654 return _shift && KoreanShiftKeys.TryGetValue(key, out var koreanShift) ? koreanShift : korean;
655
656 if (_shift && ShiftKeys.TryGetValue(key, out var shifted))
657 return shifted;
658
659 if (key.Length == 1 && char.IsLetter(key[0]))
660 return _shift ^ _capsLock ? key.ToUpperInvariant() : key.ToLowerInvariant();
661
662 return key;
663 }
664
673 private static readonly Dictionary<string, string> ShiftKeys = new()
674 {
675 ["`"] = "~", ["1"] = "!", ["2"] = "@", ["3"] = "#", ["4"] = "$",
676 ["5"] = "%", ["6"] = "^", ["7"] = "&", ["8"] = "*", ["9"] = "(",
677 ["0"] = ")", ["-"] = "_", ["="] = "+", ["["] = "{", ["]"] = "}",
678 ["\\"] = "|", [";"] = ":", ["'"] = "\"", [","] = "<", ["."] = ">",
679 ["/"] = "?",
680 };
681
690 private static readonly Dictionary<string, string> KoreanKeys = new()
691 {
692 ["q"] = "ㅂ", ["w"] = "ㅈ", ["e"] = "ㄷ", ["r"] = "ㄱ", ["t"] = "ㅅ",
693 ["y"] = "ㅛ", ["u"] = "ㅕ", ["i"] = "ㅑ", ["o"] = "ㅐ", ["p"] = "ㅔ",
694 ["a"] = "ㅁ", ["s"] = "ㄴ", ["d"] = "ㅇ", ["f"] = "ㄹ", ["g"] = "ㅎ",
695 ["h"] = "ㅗ", ["j"] = "ㅓ", ["k"] = "ㅏ", ["l"] = "ㅣ",
696 ["z"] = "ㅋ", ["x"] = "ㅌ", ["c"] = "ㅊ", ["v"] = "ㅍ", ["b"] = "ㅠ",
697 ["n"] = "ㅜ", ["m"] = "ㅡ",
698 };
699
708 private static readonly Dictionary<string, string> KoreanShiftKeys = new()
709 {
710 ["q"] = "ㅃ", ["w"] = "ㅉ", ["e"] = "ㄸ", ["r"] = "ㄲ", ["t"] = "ㅆ",
711 ["o"] = "ㅒ", ["p"] = "ㅖ",
712 };
713
738 private sealed record KeyButton(Button Button, string BaseText);
739
780 private sealed record KeySpec(KeyKind Kind, string Label, int Width, Action? Action)
781 {
814 public static KeySpec Text(string label, int width = 54) => new(KeyKind.Text, label, width, null);
815
857 public static KeySpec Command(string label, int width, Action action) => new(KeyKind.Command, label, width, action);
858 }
859
868 private enum KeyKind
869 {
888 }
889}
static readonly Dictionary< string, string > KoreanShiftKeys
static readonly Dictionary< string, string > ShiftKeys
record KeyButton(Button Button, string BaseText)
void ReplaceTextTail(int replaceCount, string text)
static readonly Dictionary< string, string > KoreanKeys