From 7591fbed74c1b78d2a8f7b4ca42a9c09c0701b95 Mon Sep 17 00:00:00 2001 From: Msystech Date: Wed, 12 Aug 2026 10:10:06 +0900 Subject: [PATCH] =?UTF-8?q?[200]=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=99=94=201/2=20=E2=80=94=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=B0=94=C2=B7=EC=B0=BD=20=EA=B8=B0=EB=B3=B8=EA=B0=92?= =?UTF-8?q?=C2=B7=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=83=89=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [200]SheetMe Studio Designer 와 6개 표면을 대조한 결과, 뼈대는 이미 같았다 — WindowChrome 파라미터, 5열 골격, 색 토큰 다크·라이트 64개 값 전부, Button/TextBox/Menu/ TabItem/ComboBox/CheckBox 등 스타일이 바이트 단위로 일치한다. 실제 격차는 "껍데기 안에 무엇이 들어가는가" 셋이었다: 스크롤바, 타이틀바 구성, 좌측 패널·인스펙터 정보 밀도. 이번 커밋은 그중 전 화면에 깔려 체감이 가장 큰 스크롤바와 테마 기반을 맞춘다. [스크롤바] [200] 통일 규격 이식 — 9px 폭(기존 11px), 상하 14px 화살표 버튼, 막대 CornerRadius 5. 막대 색은 토큰이 아니라 리터럴 중립 그레이(#8A8F96/#A6ACB3/#C4CAD0)다 — 다크·라이트 양쪽에서 같은 회색으로 보여야 배경에 따라 튀지 않는다([200] 과 동일한 의도적 선택). MinThumbTrack.cs 를 [200] src/Ui/SheetMe.Ui 에서 복사 이식(vendoring, 헤더에 동기화 의무 명시). 기본 WPF Track 은 Thumb.MinHeight 를 무시해 항목이 많은 목록에서 막대가 실처럼 가늘어지는데, 기존 ScrollThumb 의 MinHeight 48 이 바로 그래서 먹지 않고 있었다. MinThumbTrack 이 arrange 를 직접 해 최소 길이 44px 를 보장한다. [창 기본값] Window 암시 스타일에 FontSize 13 + Foreground B.Ink 추가, Background 를 B.Panel → B.AppBg 로. 이게 없어 대화상자 8종이 WPF 기본 12px + 시스템 검정으로 렌더돼 메인 셸(13px)과 결이 어긋나 있었다. 토큰 참조는 DynamicResource 유지 — [200] 의 StaticResource 를 그대로 베끼면 다크에서 값이 굳는다. [아이콘 색 인자] 컨버터 ConverterParameter 를 "크기" 단독에서 "크기|브러시키" 로 확장. 구분자가 없으면 종전 동작(크기만, B.Muted)을 그대로 유지해 기존 호출부 7곳이 안전하다. 지금까지 B.Muted 가 하드코딩돼 있어 활성 문서 탭 아이콘 강조나 레이어 타입 아이콘 색 구분이 구조적으로 불가능했다. 후속 배치의 선행 조건이다. 팔레트 아이콘 매핑도 정리 — MaskedTextBox 가 TextBox 와 같은 아이콘이라 구분이 안 됐다. [그 외] DocTabItem 비활성 탭에 하단 테두리를 넣고 활성만 끊어 크롬식 룩으로. BarToolToggle(플로팅 바 선택/손 — 켜짐을 B.Accent2 로 은은하게) 과 MiniToggle 키 스타일 신설. 암시 ToggleButton 스타일은 인스펙터·레이어 토글도 쓰므로 손대지 않고 키로 분리했다. B.FloatShadow 토큰 신설 — 플로팅 바 그림자가 고정값 하나라 라이트에서 흰 배경에 검은 그림자가 그대로 얹혀 있었다. 다크 20/3/0.5, 라이트 26/5/0.14 로 분리. 기존 34개 토큰 값은 손대지 않았다. 검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0. **종이 렌더 P062 픽셀 대조 차이 0** — 암시 스타일 변경이 용지에 새지 않음을 확인. x:Key 없는 암시 스타일 개수도 이식 전과 동일(25개)하게 유지했다. 남은 배치: C(인스펙터 접이식 섹션·세그 토글) · D(좌측 패널·타이틀바·빈 상태) · E(선택 오버레이) · F(캔버스 크롬 — 종이에 닿는 유일한 배치) · G(토스트·ThemedDialog). Co-Authored-By: Claude Fable 5 --- .../Controls/MinThumbTrack.cs | 106 +++++++++++ .../Controls/PaletteIconCatalog.cs | 53 ++++-- .../Themes/DesignerTheme.xaml | 176 ++++++++++++++---- src/SheetMe.Designer/Themes/Tokens.Dark.xaml | 3 + src/SheetMe.Designer/Themes/Tokens.Light.xaml | 3 + src/SheetMe.Designer/Views/MainView.xaml | 11 +- 6 files changed, 295 insertions(+), 57 deletions(-) create mode 100644 src/SheetMe.Designer/Controls/MinThumbTrack.cs diff --git a/src/SheetMe.Designer/Controls/MinThumbTrack.cs b/src/SheetMe.Designer/Controls/MinThumbTrack.cs new file mode 100644 index 0000000..b5af102 --- /dev/null +++ b/src/SheetMe.Designer/Controls/MinThumbTrack.cs @@ -0,0 +1,106 @@ +// 출처: [200]SheetMe src/Ui/SheetMe.Ui/MinThumbTrack.cs 복사 이식(vendoring). +// 두 저장소가 분리돼 ProjectReference 가 불가능하고, 스크롤 썸 산술은 변할 이유가 없는 고정 자산이다. +// 원본이 바뀌면 여기도 함께 갱신할 것. +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; + +namespace SheetMe.Designer.Controls; + +/// +/// 썸(thumb) 최소 길이를 '확실히' 보장하는 Track. +/// 기본 WPF Track 은 콘텐츠가 많을수록 썸을 비례로 줄이며 Thumb.MinHeight 를 반영하지 않아 +/// (아이템 1000+ 리스트에서 실처럼 가늘어짐), 여기서 직접 arrange 하여 하한()을 둔다. +/// 드래그(Thumb.DragDelta → ScrollBar 이 Track.ValueFromDistance 호출)도 자체 density 로 정확히 매핑한다. +/// 스크롤바 표준 동작: 세로는 value=Min→썸 위, 가로는 value=Min→썸 왼쪽(IsDirectionReversed 는 직접 처리). +/// +public class MinThumbTrack : Track +{ + public static readonly DependencyProperty MinThumbLengthProperty = + DependencyProperty.Register(nameof(MinThumbLength), typeof(double), typeof(MinThumbTrack), + new FrameworkPropertyMetadata(44.0, FrameworkPropertyMetadataOptions.AffectsArrange)); + + /// 썸 최소 길이(px). 트랙이 이보다 짧으면 트랙 길이로 클램프. + public double MinThumbLength + { + get => (double)GetValue(MinThumbLengthProperty); + set => SetValue(MinThumbLengthProperty, value); + } + + private double _density; // 값/픽셀 (드래그 매핑용) + private double _thumbLength; // 현재 썸 길이(px) + + protected override Size ArrangeOverride(Size finalSize) + { + double viewport = ViewportSize; + var thumb = Thumb; + if (double.IsNaN(viewport) || thumb is null) + return base.ArrangeOverride(finalSize); // 슬라이더 모드 등은 기본 동작 + + bool vertical = Orientation == Orientation.Vertical; + double trackLength = vertical ? finalSize.Height : finalSize.Width; + if (trackLength <= 0.0) return finalSize; + + double min = Minimum, max = Maximum; + double range = Math.Max(0.0, max - min); + double value = Math.Max(min, Math.Min(max, Value)); + viewport = Math.Max(0.0, viewport); + + double thumbLength, decreaseLength; + if (range <= 0.0 || viewport <= 0.0) + { + thumbLength = trackLength; // 스크롤 불필요 → 썸이 트랙 전체 + decreaseLength = 0.0; + _density = 0.0; + } + else + { + double extent = range + viewport; + thumbLength = trackLength * viewport / extent; // 비례 크기 + double minThumb = Math.Min(MinThumbLength, trackLength); // 트랙보다 길 수 없음 + if (thumbLength < minThumb) thumbLength = minThumb; // ← 하한 강제 + if (thumbLength > trackLength) thumbLength = trackLength; + + double remaining = Math.Max(0.0, trackLength - thumbLength); + _density = remaining > 0.0 ? range / remaining : 0.0; + decreaseLength = remaining * ((value - min) / range); // 시작(위/왼쪽)~썸 사이 여백 + } + _thumbLength = thumbLength; + double increaseLength = Math.Max(0.0, trackLength - thumbLength - decreaseLength); + + var dec = DecreaseRepeatButton; + var inc = IncreaseRepeatButton; + if (vertical) + { + double w = finalSize.Width; + dec?.Arrange(new Rect(0, 0, w, decreaseLength)); + thumb.Arrange(new Rect(0, decreaseLength, w, thumbLength)); + inc?.Arrange(new Rect(0, decreaseLength + thumbLength, w, increaseLength)); + } + else + { + double h = finalSize.Height; + dec?.Arrange(new Rect(0, 0, decreaseLength, h)); + thumb.Arrange(new Rect(decreaseLength, 0, thumbLength, h)); + inc?.Arrange(new Rect(decreaseLength + thumbLength, 0, increaseLength, h)); + } + return finalSize; + } + + /// 썸 드래그 거리 → 값 변화. 아래/오른쪽으로 끌면 value 증가(스크롤 내려감). + public override double ValueFromDistance(double horizontal, double vertical) + { + double dist = Orientation == Orientation.Vertical ? vertical : horizontal; + return dist * _density; + } + + public override double ValueFromPoint(Point pt) + { + bool vertical = Orientation == Orientation.Vertical; + double trackLength = vertical ? ActualHeight : ActualWidth; + double remaining = Math.Max(1.0, trackLength - _thumbLength); + double pos = (vertical ? pt.Y : pt.X) - _thumbLength / 2.0; + double f = Math.Max(0.0, Math.Min(1.0, pos / remaining)); + return Minimum + f * (Maximum - Minimum); + } +} diff --git a/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs b/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs index 7eb9248..8714946 100644 --- a/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs +++ b/src/SheetMe.Designer/Controls/PaletteIconCatalog.cs @@ -16,17 +16,18 @@ public static class PaletteIconCatalog ["Label"] = ("type", "표시"), ["PictureBox"] = ("image", "표시"), ["Line"] = ("minus", "표시"), + // MaskedTextBox 는 TextBox 와 아이콘이 겹쳐 구분이 안 됐다 — [200] 어휘로 분리 ["TextBox"] = ("text-cursor-input", "입력"), - ["MaskedTextBox"] = ("text-cursor-input", "입력"), - ["ComboBox"] = ("chevron-down", "입력"), - ["ListBox"] = ("list", "입력"), + ["MaskedTextBox"] = ("type", "입력"), + ["ComboBox"] = ("list", "입력"), + ["ListBox"] = ("list-checks", "입력"), ["CheckList"] = ("list-checks", "입력"), ["DateTimePicker"] = ("calendar", "입력"), ["CalcBox"] = ("sigma", "입력"), ["CheckBox"] = ("square-check", "선택"), ["RadioButton"] = ("circle-dot", "선택"), - ["Panel"] = ("box", "컨테이너"), - ["GroupBox"] = ("package", "컨테이너"), + ["Panel"] = ("package", "컨테이너"), + ["GroupBox"] = ("box", "컨테이너"), ["Button"] = ("mouse-pointer-click", "동작/데이터"), ["DataTable"] = ("table", "동작/데이터"), }; @@ -41,7 +42,39 @@ public static class PaletteIconCatalog #endregion } -/// 팔레트 타입명 → Lucide 아이콘 비주얼(FrameworkElement) 컨버터. parameter=크기(기본 18) +/// +/// 아이콘 컨버터 공용 파라미터 해석 — "크기" 또는 "크기|브러시키". +/// +/// 브러시 키를 생략하면 종전처럼 B.Muted 를 쓴다(하위호환). 색을 인자로 받을 수 있어야 +/// 활성 문서 탭 아이콘을 강조색으로, 레이어 타입 아이콘을 본문색으로 굽는 것이 가능해진다. +/// +internal static class IconParam +{ + public static (double Size, Brush Brush) Parse(object? parameter, double defaultSize) + { + var size = defaultSize; + var key = "B.Muted"; + + if (parameter is string text && text.Length > 0) + { + var bar = text.IndexOf('|'); + var sizePart = bar >= 0 ? text[..bar] : text; + if (bar >= 0 && bar + 1 < text.Length) + { + key = text[(bar + 1)..].Trim(); + } + if (double.TryParse(sizePart, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) + { + size = parsed; + } + } + + var brush = System.Windows.Application.Current?.TryFindResource(key) as Brush ?? Brushes.Gray; + return (size, brush); + } +} + +/// 팔레트 타입명 → Lucide 아이콘 비주얼(FrameworkElement) 컨버터. parameter="크기" 또는 "크기|브러시키"(기본 18, B.Muted) public sealed class TypeToIconConverter : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) @@ -50,8 +83,7 @@ public sealed class TypeToIconConverter : IValueConverter { return null; } - var size = parameter is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 18; - var brush = System.Windows.Application.Current.TryFindResource("B.Muted") as Brush ?? Brushes.Gray; + var (size, brush) = IconParam.Parse(parameter, 18); return LucideIcons.Icon(PaletteIconCatalog.IconOf(type), size, brush); } @@ -82,7 +114,7 @@ public sealed class InverseBoolConverter : IValueConverter /// /// Lucide 아이콘명 → 비주얼 컨버터(문서 탭 file-text, 플로팅 바 layout-grid 등 고정 아이콘용). /// Binding Source 에 아이콘명 문자열을 넣어 사용 — 항목마다 새 비주얼이 생성되어 공유 부모 충돌이 없다. -/// parameter=크기(기본 14). +/// parameter="크기" 또는 "크기|브러시키"(기본 14, B.Muted). /// public sealed class IconNameToVisualConverter : IValueConverter { @@ -92,8 +124,7 @@ public sealed class IconNameToVisualConverter : IValueConverter { return null; } - var size = parameter is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v : 14; - var brush = System.Windows.Application.Current.TryFindResource("B.Muted") as Brush ?? Brushes.Gray; + var (size, brush) = IconParam.Parse(parameter, 14); return LucideIcons.Icon(name, size, brush); } diff --git a/src/SheetMe.Designer/Themes/DesignerTheme.xaml b/src/SheetMe.Designer/Themes/DesignerTheme.xaml index 82670fe..3b3f695 100644 --- a/src/SheetMe.Designer/Themes/DesignerTheme.xaml +++ b/src/SheetMe.Designer/Themes/DesignerTheme.xaml @@ -1,6 +1,7 @@ + xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework" + xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"> - + @@ -386,8 +391,9 @@ + @@ -400,6 +406,7 @@ + @@ -461,6 +468,34 @@ + + + + + + - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml index e344cba..858a998 100644 --- a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml +++ b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml @@ -43,4 +43,7 @@ + + + diff --git a/src/SheetMe.Designer/Themes/Tokens.Light.xaml b/src/SheetMe.Designer/Themes/Tokens.Light.xaml index e1e06ca..f7b8b83 100644 --- a/src/SheetMe.Designer/Themes/Tokens.Light.xaml +++ b/src/SheetMe.Designer/Themes/Tokens.Light.xaml @@ -43,4 +43,7 @@ + + + diff --git a/src/SheetMe.Designer/Views/MainView.xaml b/src/SheetMe.Designer/Views/MainView.xaml index 97f3c82..149c1ff 100644 --- a/src/SheetMe.Designer/Views/MainView.xaml +++ b/src/SheetMe.Designer/Views/MainView.xaml @@ -442,20 +442,21 @@ + - + - - + + Content="{Binding Source=hand, Converter={StaticResource IconName}, ConverterParameter=18|B.Ink}"/>