Preferences redesign 6/11: Layout (#9878) (#10638)

* Preferences: preserve Layout controls alongside the other redesigned panels

* Tests: locate the redesigned Layout checkbox by its visible label
This commit is contained in:
Nathan Dane
2026-11-03 17:34:24 +00:00
committed by GitHub
parent 97cedc77e8
commit 92a9a5e0f0
27 changed files with 1371 additions and 429 deletions
+2 -2
View File
@@ -2261,7 +2261,7 @@
"prefs.bodies_folders": "مجلدات الأجسام",
"prefs.bodies_folders_always": "دائمًا",
"prefs.bodies_folders_never": "أبدًا",
"prefs.bodies_folders_several": "عند تعدد الأجسام فقط",
"prefs.bodies_folders_several": "عدة",
"prefs.bottom": "أسفل",
"prefs.bottom_centre": "أسفل الوسط",
"prefs.bottom_right": "أسفل اليمين",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "يبدأ السجل على اليسار أو تحت المستعرض بمجلد الأجسام للمكوّن النشط وبالأصل، مع أيقونات الإظهار والقوائم كما في المستعرض. لا يتغير السجل في الأسفل.",
"prefs.history_play_button": "زر تشغيل السجل في عناصر تحكم السجل",
"prefs.history_play_button_note": "يضيف تشغيل السجل بين الرجوع خطوة والتقدم خطوة في عناصر تحكم السجل. يمكنك أيضًا النقر بزر الماوس الأيمن على عناصر التحكم لإظهاره أو إخفائه.",
"prefs.history_sketches": "الرسومات",
"prefs.history_sketches": "الرسومات في السجل",
"prefs.history_sketches_in_order": "بالترتيب",
"prefs.history_sketches_note": "عند ضبط الرسومات على «تحت ميزتها»، يعرض السجل على اليسار كل رسم بمستوى واحد للداخل، تحت أول ميزة بُنيت منه، متى كان عريضًا بما يكفي لإظهار الأسماء. أما الخط الزمني في الأسفل فيحتفظ دائمًا بترتيب الإنشاء.",
"prefs.history_sketches_under_feature": "تحت ميزتها",
+2 -2
View File
@@ -2244,7 +2244,7 @@
"prefs.bodies_folders": "Körperordner",
"prefs.bodies_folders_always": "Immer",
"prefs.bodies_folders_never": "Nie",
"prefs.bodies_folders_several": "Nur bei mehreren Körpern",
"prefs.bodies_folders_several": "Mehrere",
"prefs.bottom": "Unten",
"prefs.bottom_centre": "Unten mittig",
"prefs.bottom_right": "Unten rechts",
@@ -2359,7 +2359,7 @@
"prefs.history_origin_bodies_note": "Der Verlauf links oder unter dem Browser beginnt mit dem Körper-Ordner der aktiven Komponente und dem Ursprung, mit Augen und Menüs wie im Browser. Der Verlauf unten bleibt unverändert.",
"prefs.history_play_button": "Schaltfläche „Verlauf abspielen“ in den Verlaufssteuerelementen",
"prefs.history_play_button_note": "Fügt „Verlauf abspielen“ zwischen „Einen Schritt zurück“ und „Einen Schritt vor“ in die Verlaufssteuerelemente ein. Mit einem Rechtsklick auf die Steuerelemente lässt sich die Schaltfläche auch ein- oder ausblenden.",
"prefs.history_sketches": "Skizzen",
"prefs.history_sketches": "Skizzen im Verlauf",
"prefs.history_sketches_in_order": "In Erstellungsreihenfolge",
"prefs.history_sketches_note": "Wenn „Skizzen“ auf „Unter dem zugehörigen Feature“ eingestellt ist, wird jede Skizze im linken Verlauf eine Ebene unter dem ersten daraus erstellten Feature angezeigt, sofern der Bereich breit genug für die Namen ist. Die Timeline unten zeigt weiterhin die Reihenfolge, in der die Elemente erstellt wurden.",
"prefs.history_sketches_under_feature": "Unter dem zugehörigen Feature",
+3 -3
View File
@@ -2261,14 +2261,14 @@
"prefs.bodies_folders": "Bodies folders",
"prefs.bodies_folders_always": "Always",
"prefs.bodies_folders_never": "Never",
"prefs.bodies_folders_several": "Several bodies only",
"prefs.bodies_folders_several": "Several",
"prefs.bottom": "Bottom",
"prefs.bottom_centre": "Bottom center",
"prefs.bottom_right": "Bottom right",
"prefs.browser": "Browser",
"prefs.browser_components_open": "Components start open",
"prefs.browser_components_open_note": "Every component in the browser starts open, as before, instead of closed until you open it.",
"prefs.browser_eye_left": "Eye next to the fold arrow",
"prefs.browser_eye_left": "Eye beside the fold arrow",
"prefs.browser_eye_left_note": "Browser rows show their show/hide eye next to the fold arrow, as before, instead of in a column at the right end of the rows.",
"prefs.browser_reveal_on_hover": "Reveal on hover",
"prefs.browser_show_note": "Shown keeps the browser on screen. Reveal on hover keeps it out of sight until the pointer comes over its place or the thin tab at its left edge, and it stays open while the pointer is inside it or a menu from it is open.",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "The History on the left or under the browser starts with the active component's Bodies folder and the Origin, with their eyes and menus as in the browser. The History at the bottom is unchanged.",
"prefs.history_play_button": "Play History button in the History controls",
"prefs.history_play_button_note": "Puts Play History between Go back one step and Go forward one step in the History controls. You can also right-click the controls to show or hide it.",
"prefs.history_sketches": "Sketches",
"prefs.history_sketches": "Sketches in History",
"prefs.history_sketches_in_order": "In order",
"prefs.history_sketches_note": "With Sketches set to Under their feature, the History at the left lists each sketch one level in, under the first feature built from it, whenever it is wide enough for names. The timeline at the bottom always keeps the creation order.",
"prefs.history_sketches_under_feature": "Under their feature",
+2 -2
View File
@@ -2261,7 +2261,7 @@
"prefs.bodies_folders": "תיקיות גופים",
"prefs.bodies_folders_always": "תמיד",
"prefs.bodies_folders_never": "אף פעם",
"prefs.bodies_folders_several": "רק כשיש כמה גופים",
"prefs.bodies_folders_several": "כמה",
"prefs.bottom": "למטה",
"prefs.bottom_centre": "למטה במרכז",
"prefs.bottom_right": "למטה מימין",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "ההיסטוריה משמאל או מתחת לדפדפן מתחילה בתיקיית הגופים של הרכיב הפעיל ובראשית, עם סמלי העין והתפריטים כמו בדפדפן. ההיסטוריה למטה לא משתנה.",
"prefs.history_play_button": "כפתור הפעלת ציר הזמן בפקדי ציר הזמן",
"prefs.history_play_button_note": "מוסיף את הפעלת ציר הזמן בין צעד אחד אחורה לצעד אחד קדימה בפקדי ציר הזמן. אפשר גם ללחוץ לחיצה ימנית על הפקדים כדי להציג או להסתיר אותו.",
"prefs.history_sketches": "סקיצות",
"prefs.history_sketches": "סקיצות בציר הזמן",
"prefs.history_sketches_in_order": "לפי הסדר",
"prefs.history_sketches_note": "כשהסקיצות מוגדרות ל„מתחת לפיצ'ר שלהן”, ציר הזמן משמאל מציג כל סקיצה רמה אחת פנימה, מתחת לפיצ'ר הראשון שנבנה ממנה, בכל פעם שהוא רחב מספיק להצגת שמות. ציר הזמן בתחתית שומר תמיד על סדר היצירה.",
"prefs.history_sketches_under_feature": "מתחת לפיצ'ר שלהן",
+3 -3
View File
@@ -2261,14 +2261,14 @@
"prefs.bodies_folders": "ボディフォルダー",
"prefs.bodies_folders_always": "常に表示",
"prefs.bodies_folders_never": "表示しない",
"prefs.bodies_folders_several": "複数ボディのみ",
"prefs.bodies_folders_several": "複数",
"prefs.bottom": "下",
"prefs.bottom_centre": "下中央",
"prefs.bottom_right": "右下",
"prefs.browser": "ブラウザー",
"prefs.browser_components_open": "コンポーネントを最初から展開",
"prefs.browser_components_open_note": "ブラウザーのすべてのコンポーネントを以前と同じく最初から展開して表示します。オフの場合は展開するまで折りたたまれています。",
"prefs.browser_eye_left": "表示切り替えを折りたたみ矢印の隣に表示",
"prefs.browser_eye_left": "折りたたみ矢印の隣の目のアイコン",
"prefs.browser_eye_left_note": "ブラウザーの各行の表示切り替えを、行の右端の列ではなく、以前のように折りたたみ矢印の隣に表示します。",
"prefs.browser_reveal_on_hover": "ホバーで表示",
"prefs.browser_show_note": "「表示」はブラウザーを常に画面に表示します。「ホバーで表示」はポインターがその位置か左端の細いタブに来るまで隠し、ポインターが中にある間やそのメニューが開いている間は表示したままにします。",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "左側またはブラウザー下の履歴の先頭に、アクティブなコンポーネントのボディフォルダーと原点を、ブラウザーと同じ表示切り替えとメニュー付きで表示します。下部の履歴は変わりません。",
"prefs.history_play_button": "履歴コントロールに「履歴を再生」ボタンを表示",
"prefs.history_play_button_note": "履歴コントロールの「1ステップ戻る」と「1ステップ進む」の間に「履歴を再生」を追加します。コントロールを右クリックして表示/非表示を切り替えることもできます。",
"prefs.history_sketches": "スケッチ",
"prefs.history_sketches": "履歴のスケッチ",
"prefs.history_sketches_in_order": "作成順",
"prefs.history_sketches_note": "スケッチ:「フィーチャーの下」では、左側の履歴に名前が表示される幅があるとき、スケッチをそれから作成した最初のフィーチャーの下に 1 段字下げして表示します。下部のタイムラインは常に作成順です。",
"prefs.history_sketches_under_feature": "フィーチャーの下",
+2 -2
View File
@@ -1653,14 +1653,14 @@
"prefs.bodies_folders": "바디 폴더",
"prefs.bodies_folders_always": "항상 표시",
"prefs.bodies_folders_never": "표시 안 함",
"prefs.bodies_folders_several": "바디가 여러 개일 때만",
"prefs.bodies_folders_several": "여러 개",
"prefs.bottom": "아래쪽",
"prefs.bottom_centre": "하단 가운데",
"prefs.bottom_right": "오른쪽 아래",
"prefs.browser": "브라우저",
"prefs.browser_components_open": "구성요소를 펼친 상태로 시작",
"prefs.browser_components_open_note": "브라우저의 모든 구성요소가 이전처럼 펼쳐진 상태로 시작합니다. 끄면 직접 펼칠 때까지 접혀 있습니다.",
"prefs.browser_eye_left": "표시 아이콘을 접기 화살표 옆에 표시",
"prefs.browser_eye_left": "접기 화살표 옆의 눈 아이콘",
"prefs.browser_eye_left_note": "브라우저 행의 표시 아이콘을 행 오른쪽 끝의 열이 아니라 이전처럼 접기 화살표 옆에 표시합니다.",
"prefs.built_in": "기본 제공",
"prefs.button_left": "왼쪽 드래그",
@@ -0,0 +1,147 @@
{
"ar": {
"prefs.layout_page.active_commands": "تمييز الأوامر النشطة",
"prefs.layout_page.always": "دائمًا",
"prefs.layout_page.card": "بطاقة",
"prefs.layout_page.column": "عمود",
"prefs.layout_page.hover": "عند مرور المؤشر",
"prefs.layout_page.icons_labels": "الأيقونات + التسميات",
"prefs.layout_page.ok_cancel": "موافق وإلغاء",
"prefs.layout_page.on_hover": "عند مرور المؤشر",
"prefs.layout_page.open": "فتح",
"prefs.layout_page.origin_bodies": "الأصل والأجسام في الأعلى",
"prefs.layout_page.panels": "اللوحات",
"prefs.layout_page.pinned_menus": "استبعاد الأوامر المثبتة من القوائم",
"prefs.layout_page.play_history": "زر تشغيل السجل",
"prefs.layout_page.slot_count": "{count} من {max} خانة",
"prefs.layout_page.view_corner": "أيضًا في زاوية العرض",
"prefs.layout_page.window": "النافذة"
},
"de": {
"prefs.layout_page.active_commands": "Aktive Befehle hervorheben",
"prefs.layout_page.always": "Immer",
"prefs.layout_page.card": "Karte",
"prefs.layout_page.column": "Spalte",
"prefs.layout_page.hover": "Bei Mauszeiger",
"prefs.layout_page.icons_labels": "Symbole + Beschriftungen",
"prefs.layout_page.ok_cancel": "OK und Abbrechen",
"prefs.layout_page.on_hover": "Bei Mauszeiger",
"prefs.layout_page.open": "Öffnen",
"prefs.layout_page.origin_bodies": "Ursprung und Körper oben",
"prefs.layout_page.panels": "Bereiche",
"prefs.layout_page.pinned_menus": "Angeheftete Befehle aus Menüs ausblenden",
"prefs.layout_page.play_history": "Verlaufswiedergabe",
"prefs.layout_page.slot_count": "{count} von {max} Plätzen",
"prefs.layout_page.view_corner": "Auch in der Ansichtsecke",
"prefs.layout_page.window": "Fenster"
},
"en": {
"prefs.layout_page.active_commands": "Highlight active commands",
"prefs.layout_page.always": "Always",
"prefs.layout_page.card": "Card",
"prefs.layout_page.column": "Column",
"prefs.layout_page.hover": "Hover",
"prefs.layout_page.icons_labels": "Icons + labels",
"prefs.layout_page.ok_cancel": "OK and Cancel",
"prefs.layout_page.on_hover": "On hover",
"prefs.layout_page.open": "Open",
"prefs.layout_page.origin_bodies": "Origin and bodies on top",
"prefs.layout_page.panels": "Panels",
"prefs.layout_page.pinned_menus": "Pinned commands out of menus",
"prefs.layout_page.play_history": "Play History button",
"prefs.layout_page.slot_count": "{count} of {max} slots",
"prefs.layout_page.view_corner": "Also in the view's corner",
"prefs.layout_page.window": "Window"
},
"he": {
"prefs.layout_page.active_commands": "הדגשת פקודות פעילות",
"prefs.layout_page.always": "תמיד",
"prefs.layout_page.card": "כרטיס",
"prefs.layout_page.column": "עמודה",
"prefs.layout_page.hover": "מעבר עכבר",
"prefs.layout_page.icons_labels": "סמלים + תוויות",
"prefs.layout_page.ok_cancel": "אישור וביטול",
"prefs.layout_page.on_hover": "בעת מעבר עכבר",
"prefs.layout_page.open": "פתיחה",
"prefs.layout_page.origin_bodies": "הראשית והגופים למעלה",
"prefs.layout_page.panels": "חלוניות",
"prefs.layout_page.pinned_menus": "השמטת פקודות מוצמדות מהתפריטים",
"prefs.layout_page.play_history": "כפתור הפעלת ציר הזמן",
"prefs.layout_page.slot_count": "{count} מתוך {max} משבצות",
"prefs.layout_page.view_corner": "גם בפינת התצוגה",
"prefs.layout_page.window": "חלון"
},
"ja": {
"prefs.layout_page.active_commands": "アクティブなコマンドを強調",
"prefs.layout_page.always": "常に",
"prefs.layout_page.card": "カード",
"prefs.layout_page.column": "列",
"prefs.layout_page.hover": "ホバー時",
"prefs.layout_page.icons_labels": "アイコン+ラベル",
"prefs.layout_page.ok_cancel": "OK とキャンセル",
"prefs.layout_page.on_hover": "ホバー時",
"prefs.layout_page.open": "開く位置",
"prefs.layout_page.origin_bodies": "原点とボディを上に表示",
"prefs.layout_page.panels": "パネル",
"prefs.layout_page.pinned_menus": "固定コマンドをメニューから除外",
"prefs.layout_page.play_history": "履歴の再生ボタン",
"prefs.layout_page.slot_count": "{max} スロット中 {count}",
"prefs.layout_page.view_corner": "ビューの隅にも表示",
"prefs.layout_page.window": "ウィンドウ"
},
"ko": {
"prefs.history_sketches": "히스토리의 스케치",
"prefs.layout_page.active_commands": "활성 명령 강조",
"prefs.layout_page.always": "항상",
"prefs.layout_page.card": "카드",
"prefs.layout_page.column": "열",
"prefs.layout_page.hover": "마우스를 올릴 때",
"prefs.layout_page.icons_labels": "아이콘 + 레이블",
"prefs.layout_page.ok_cancel": "확인과 취소",
"prefs.layout_page.on_hover": "마우스를 올릴 때",
"prefs.layout_page.open": "열기",
"prefs.layout_page.origin_bodies": "원점과 바디를 위에 표시",
"prefs.layout_page.panels": "패널",
"prefs.layout_page.pinned_menus": "고정 명령을 메뉴에서 제외",
"prefs.layout_page.play_history": "히스토리 재생 버튼",
"prefs.layout_page.slot_count": "슬롯 {count} / {max}개",
"prefs.layout_page.view_corner": "뷰 모서리에도 표시",
"prefs.layout_page.window": "창"
},
"vi": {
"prefs.layout_page.active_commands": "Tô sáng lệnh đang bật",
"prefs.layout_page.always": "Luôn luôn",
"prefs.layout_page.card": "Thẻ",
"prefs.layout_page.column": "Cột",
"prefs.layout_page.hover": "Rê chuột",
"prefs.layout_page.icons_labels": "Biểu tượng + nhãn",
"prefs.layout_page.ok_cancel": "Đồng ý và Hủy bỏ",
"prefs.layout_page.on_hover": "Khi rê chuột",
"prefs.layout_page.open": "Mở",
"prefs.layout_page.origin_bodies": "Gốc tọa độ và thân khối ở trên cùng",
"prefs.layout_page.panels": "Bảng điều khiển",
"prefs.layout_page.pinned_menus": "Loại lệnh đã ghim khỏi menu",
"prefs.layout_page.play_history": "Nút Phát lịch sử",
"prefs.layout_page.slot_count": "{count}/{max} ô",
"prefs.layout_page.view_corner": "Cũng ở góc khung nhìn",
"prefs.layout_page.window": "Cửa sổ"
},
"zh": {
"prefs.layout_page.active_commands": "突出显示活动命令",
"prefs.layout_page.always": "始终",
"prefs.layout_page.card": "卡片",
"prefs.layout_page.column": "列",
"prefs.layout_page.hover": "悬停",
"prefs.layout_page.icons_labels": "图标+标签",
"prefs.layout_page.ok_cancel": "确定和取消",
"prefs.layout_page.on_hover": "悬停时",
"prefs.layout_page.open": "打开位置",
"prefs.layout_page.origin_bodies": "原点和实体置顶",
"prefs.layout_page.panels": "面板",
"prefs.layout_page.pinned_menus": "从菜单中排除固定命令",
"prefs.layout_page.play_history": "播放历史按钮",
"prefs.layout_page.slot_count": "{count} / {max} 个槽位",
"prefs.layout_page.view_corner": "同时在视图角落显示",
"prefs.layout_page.window": "窗口"
}
}
@@ -0,0 +1,78 @@
{
"ar": {
"navbar.custom.anchor.bottom_centre": "أسفل الوسط",
"navbar.custom.anchor.bottom_left": "أسفل اليسار",
"navbar.custom.anchor.bottom_right": "أسفل اليمين",
"navbar.custom.anchor.top_centre": "أعلى الوسط",
"navbar.custom.anchor.top_left": "أعلى اليسار",
"navbar.custom.anchor.top_right": "أعلى اليمين",
"navbar.custom.separator_hint": "فاصل بين مجموعات الأزرار. اسحبه إلى الشريط، أو اضغط على إضافة؛ يمكنك استخدام أي عدد من الفواصل."
},
"de": {
"navbar.custom.anchor.bottom_centre": "Unten mittig",
"navbar.custom.anchor.bottom_left": "Unten links",
"navbar.custom.anchor.bottom_right": "Unten rechts",
"navbar.custom.anchor.top_centre": "Oben mittig",
"navbar.custom.anchor.top_left": "Oben links",
"navbar.custom.anchor.top_right": "Oben rechts",
"navbar.custom.separator_hint": "Eine Trennlinie zwischen Gruppen von Schaltflächen. Auf die Leiste ziehen oder auf Hinzufügen klicken; beliebig oft verwendbar."
},
"en": {
"navbar.custom.anchor.bottom_centre": "Bottom center",
"navbar.custom.anchor.bottom_left": "Bottom left",
"navbar.custom.anchor.bottom_right": "Bottom right",
"navbar.custom.anchor.top_centre": "Top center",
"navbar.custom.anchor.top_left": "Top left",
"navbar.custom.anchor.top_right": "Top right",
"navbar.custom.separator_hint": "A divider between groups of buttons. Drag it onto the bar, or press Add; use as many as you like."
},
"en-GB": {
"navbar.custom.anchor.bottom_centre": "Bottom centre",
"navbar.custom.anchor.top_centre": "Top centre"
},
"he": {
"navbar.custom.anchor.bottom_centre": "למטה במרכז",
"navbar.custom.anchor.bottom_left": "למטה משמאל",
"navbar.custom.anchor.bottom_right": "למטה מימין",
"navbar.custom.anchor.top_centre": "למעלה במרכז",
"navbar.custom.anchor.top_left": "למעלה משמאל",
"navbar.custom.anchor.top_right": "למעלה מימין",
"navbar.custom.separator_hint": "מפריד בין קבוצות כפתורים. גררו אותו לסרגל או לחצו על הוספה; אפשר להשתמש בכמה מפרידים שתרצו."
},
"ja": {
"navbar.custom.anchor.bottom_centre": "下中央",
"navbar.custom.anchor.bottom_left": "左下",
"navbar.custom.anchor.bottom_right": "右下",
"navbar.custom.anchor.top_centre": "上中央",
"navbar.custom.anchor.top_left": "左上",
"navbar.custom.anchor.top_right": "右上",
"navbar.custom.separator_hint": "ボタンのグループ間の区切りです。バーにドラッグするか、「追加」を押してください。いくつでも使用できます。"
},
"ko": {
"navbar.custom.anchor.bottom_centre": "하단 가운데",
"navbar.custom.anchor.bottom_left": "왼쪽 아래",
"navbar.custom.anchor.bottom_right": "오른쪽 아래",
"navbar.custom.anchor.top_centre": "상단 가운데",
"navbar.custom.anchor.top_left": "왼쪽 위",
"navbar.custom.anchor.top_right": "오른쪽 위",
"navbar.custom.separator_hint": "버튼 그룹 사이의 구분선입니다. 막대로 끌거나 추가를 누르세요. 원하는 만큼 사용할 수 있습니다."
},
"vi": {
"navbar.custom.anchor.bottom_centre": "Dưới giữa",
"navbar.custom.anchor.bottom_left": "Dưới trái",
"navbar.custom.anchor.bottom_right": "Dưới phải",
"navbar.custom.anchor.top_centre": "Trên giữa",
"navbar.custom.anchor.top_left": "Trên trái",
"navbar.custom.anchor.top_right": "Trên phải",
"navbar.custom.separator_hint": "Đường phân cách giữa các nhóm nút. Kéo vào thanh hoặc nhấn Thêm; bạn có thể dùng bao nhiêu đường phân cách tùy ý."
},
"zh": {
"navbar.custom.anchor.bottom_centre": "底部居中",
"navbar.custom.anchor.bottom_left": "左下角",
"navbar.custom.anchor.bottom_right": "右下角",
"navbar.custom.anchor.top_centre": "顶部居中",
"navbar.custom.anchor.top_left": "左上角",
"navbar.custom.anchor.top_right": "右上角",
"navbar.custom.separator_hint": "按钮组之间的分隔线。将其拖到栏上,或按“添加”;可以按需使用任意数量。"
}
}
+2 -2
View File
@@ -2261,7 +2261,7 @@
"prefs.bodies_folders": "Thư mục thân khối",
"prefs.bodies_folders_always": "Luôn luôn",
"prefs.bodies_folders_never": "Không bao giờ",
"prefs.bodies_folders_several": "Chỉ khi có nhiều thân khối",
"prefs.bodies_folders_several": "Nhiều",
"prefs.bottom": "Dưới cùng",
"prefs.bottom_centre": "Giữa phía dưới",
"prefs.bottom_right": "Góc dưới bên phải",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "Cây Lịch sử ở bên trái hoặc dưới cây thư mục sẽ bắt đầu với thư mục Thân khối và Gốc tọa độ của cụm chi tiết đang kích hoạt, cùng biểu tượng mắt bật/tắt và trình đơn tương tự như trong cây thư mục. Dòng Lịch sử ở cạnh dưới không thay đổi.",
"prefs.history_play_button": "Nút Phát lịch sử trong các điều khiển Lịch sử",
"prefs.history_play_button_note": "Thêm Phát lịch sử vào giữa Lùi một bước và Tiến một bước trong các điều khiển Lịch sử. Bạn cũng có thể nhấp chuột phải vào các điều khiển để hiện hoặc ẩn nút này.",
"prefs.history_sketches": "Phác thảo",
"prefs.history_sketches": "Các bản phác trong Lịch sử",
"prefs.history_sketches_in_order": "Theo thứ tự tạo",
"prefs.history_sketches_note": "Khi tùy chọn Phác thảo được đặt thành Dưới tính năng tương ứng, cây Lịch sử ở bên trái sẽ liệt kê mỗi phác thảo thụt vào một cấp dưới tính năng đầu tiên được tạo từ nó (khi khung đủ rộng để hiển thị tên). Dòng thời gian ở cạnh dưới luôn giữ nguyên thứ tự tạo.",
"prefs.history_sketches_under_feature": "Dưới tính năng tương ứng",
+3 -3
View File
@@ -2261,14 +2261,14 @@
"prefs.bodies_folders": "实体文件夹",
"prefs.bodies_folders_always": "始终显示",
"prefs.bodies_folders_never": "不显示",
"prefs.bodies_folders_several": "仅限多个实体",
"prefs.bodies_folders_several": "多个",
"prefs.bottom": "底部",
"prefs.bottom_centre": "底部居中",
"prefs.bottom_right": "右下角",
"prefs.browser": "浏览器",
"prefs.browser_components_open": "组件初始展开",
"prefs.browser_components_open_note": "浏览器中的每个组件都像以前一样初始展开,而不是在您展开前保持折叠。",
"prefs.browser_eye_left": "显示切换位于折叠箭头旁",
"prefs.browser_eye_left": "折叠箭头旁的眼睛图标",
"prefs.browser_eye_left_note": "浏览器各行的显示切换像以前一样位于折叠箭头旁,而不是在行右端的一列中。",
"prefs.browser_reveal_on_hover": "悬停时显示",
"prefs.browser_show_note": "“显示”让浏览器始终留在屏幕上。“悬停时显示”会将其隐藏,直到指针移到它的位置或其左边缘的细标签上;指针在其中或其菜单打开时,它保持打开。",
@@ -2376,7 +2376,7 @@
"prefs.history_origin_bodies_note": "左侧或浏览器下方的历史以活动组件的实体文件夹和原点开头,并带有与浏览器相同的显示切换和菜单。底部的历史保持不变。",
"prefs.history_play_button": "在历史控件中显示“播放历史”按钮",
"prefs.history_play_button_note": "在历史控件的“后退一步”和“前进一步”之间添加“播放历史”。也可以右键单击这些控件来显示或隐藏它。",
"prefs.history_sketches": "草图",
"prefs.history_sketches": "历史中的草图",
"prefs.history_sketches_in_order": "按创建顺序",
"prefs.history_sketches_note": "草图:选择“位于其特征下”后,左侧历史在宽度足以显示名称时,会将草图缩进一级,列在由其创建的第一个特征下。底部时间轴始终保持创建顺序。",
"prefs.history_sketches_under_feature": "位于其特征下",
+4 -2
View File
@@ -494,7 +494,7 @@ fn navigation_board_metrics_and_state_colors_are_shared_in_both_themes() {
}
assert_eq!((NAV_BORDER, NAV_PADDING, NAV_GAP, NAV_BOTTOM, NAV_EDGE), (1.0, 3.0, 2.0, 7.0, 12.0));
assert_eq!((NAV_SEPARATOR, NAV_SEPARATOR_HEIGHT, NAV_MENU_LEFT, NAV_MENU_RIGHT, NAV_MENU_GAP, NAV_CHEVRON), (9.0, 18.0, 7.0, 6.0, 5.0, 12.0));
assert_eq!((NAV_STAGE_HEIGHT, NAV_STAGE_INSET, NAV_TILE_HEIGHT, NAV_TILE_ADD), (58.0, 12.0, 32.0, 22.0));
assert_eq!((NAV_STAGE_HEIGHT, NAV_STAGE_INSET, NAV_TILE_HEIGHT, NAV_TILE_ADD), (96.0, 12.0, 32.0, 22.0));
assert_eq!(
(NAV_MARKER_HEIGHT, NAV_GHOST_OPACITY, NAV_TILE_DRAG_OPACITY, NAV_IDLE_OPACITY, NAV_FADE_SECONDS, NAV_TOOLTIP_DELAY),
(24.0, 0.7, 0.55, 0.45, 0.12, 0.8)
@@ -589,7 +589,9 @@ fn navigation_board_position_aliases_migrate_and_editor_alignment_follows_positi
let bar = NavigationBar { size, ..bar.clone() };
let preview = editor_layout(stage, &bar, |_| 0.0);
let live = measure(stage, &bar, NavCaps::MODEL, |_| 0.0);
assert_eq!(preview.rect.center().y, stage.center().y);
assert_eq!(preview.rect.bottom(), stage.bottom() - NAV_BORDER - NAV_STAGE_INSET);
let top = editor_layout(stage, &NavigationBar { edge: crate::navigation_bar::Edge::Top, ..bar.clone() }, |_| 0.0);
assert_eq!(top.rect.top(), stage.top() + NAV_BORDER + NAV_STAGE_INSET);
assert_eq!(preview.rect.height(), NavigationMetrics::for_size(size).height());
match position {
Position::Centre => assert_eq!(preview.rect.center().x, stage.center().x),
+6 -1
View File
@@ -119,7 +119,12 @@ pub(crate) fn editor_layout(stage: Rect, prefs: &NavigationBar, label_width: imp
Position::Centre => stage.center().x - bar.rect.width() / 2.0,
Position::Right => stage.right() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - bar.rect.width(),
};
let delta = pos2(x, stage.center().y - bar.rect.height() / 2.0) - bar.rect.min;
// The same inset from the top or bottom edge as from the side, as in the live view.
let y = match prefs.edge {
navigation_bar::Edge::Top => stage.top() + ui_kit::NAV_BORDER + ui_kit::NAV_STAGE_INSET,
navigation_bar::Edge::Bottom => stage.bottom() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - bar.rect.height(),
};
let delta = pos2(x, y) - bar.rect.min;
bar.rect = bar.rect.translate(delta);
for (_, rect) in &mut bar.slots {
*rect = rect.translate(delta);
+116 -43
View File
@@ -1234,23 +1234,23 @@ preference_settings! {
/// Show command names beneath ribbon icons, independently of workspace layout.
ribbon_labels: bool = true, PAGE_LAYOUT;
/// Show engine-derived persistent selection in the ribbon; off retains neutral buttons.
persistent_ribbon_pressed: bool = true, PAGE_LAYOUT => check_id("prefs.persistent_ribbon_pressed");
persistent_ribbon_pressed: bool = true, PAGE_LAYOUT;
viewport_analysis_status: bool = true, PAGE_GRAPHICS;
viewport_section_status: bool = true, PAGE_GRAPHICS;
viewport_status_details: bool = true, PAGE_GRAPHICS;
viewport_status_summary: bool = true, PAGE_GRAPHICS;
/// Keep the SKETCH tab in the ribbon, right after MODEL, while no sketch is open; its drawing
/// tools then start a sketch first (#7904).
sketch_tab_always: bool = false, PAGE_LAYOUT => check_id("prefs.sketch_tab_always");
sketch_tab_always: bool = false, PAGE_LAYOUT;
/// Each design tab keeps its open dialog or tool, and its toolbar tab, while another design is
/// shown, and has them back when it is shown again (#9360). Off by default: a switch cancels
/// them and shows the MODEL tab.
keep_work_on_tab_switch: bool = false, PAGE_LAYOUT => check_id("prefs.keep_work_on_tab_switch");
keep_work_on_tab_switch: bool = false, PAGE_LAYOUT;
/// Hovering a result in Search commands shows the command's place in the ribbon: its tab,
/// its button or its row in a section's drop-down (#7287). A command started from the search
/// blinks there, and a running command that sits in a drop-down marks its section label. Off
/// by default: the ribbon stays as it was.
ribbon_reveal: bool = false, PAGE_LAYOUT => check_id("prefs.ribbon_reveal");
ribbon_reveal: bool = false, PAGE_LAYOUT;
/// Where a dialog's OK and Cancel sit: under its inputs (the default) or in its title row (#6708).
dialog_buttons: crate::ui_kit::DialogButtons = Default::default(), PAGE_LAYOUT;
/// Where notices and offers stack: the view's bottom-right corner (the default) or the bottom
@@ -1263,7 +1263,7 @@ preference_settings! {
ribbon_icon_grid: bool = false, PAGE_LAYOUT;
/// Leave the commands the ribbon shows as buttons (pinned ones) out of its panel drop-downs,
/// which then list only the rest (#4739).
ribbon_menus_skip_pinned: bool = false, PAGE_LAYOUT => check_tr("ui.ribbon_menus_skip_pinned");
ribbon_menus_skip_pinned: bool = false, PAGE_LAYOUT;
/// Interface scale (1 = 100 %).
ui_scale: f32 = 1.0, PAGE_APPEARANCE, accept(0.5..=2.5);
/// Interface font: an installed font family (or a font file), or empty for the built-in one
@@ -1271,10 +1271,10 @@ preference_settings! {
ui_font: String = String::new(), PAGE_APPEARANCE;
/// While a dialog or a sketch is open, OK and Cancel in the view's top right corner, with the
/// axis view moved down below them (#6709). Off by default.
view_confirm_corner: bool = false, PAGE_LAYOUT => check_id("prefs.view_confirm_corner");
view_confirm_corner: bool = false, PAGE_LAYOUT;
/// A thin bar along the window's bottom edge: what is being edited, the open sketch's state
/// and the design's units (#7243). Off by default.
status_bar: bool = false, PAGE_LAYOUT => check_id("prefs.status_bar");
status_bar: bool = false, PAGE_LAYOUT;
/// The selection's length, area or distance in the view's bottom right corner (#7000). Off by
/// default.
quick_measure: bool = false, PAGE_GRAPHICS;
@@ -1298,7 +1298,7 @@ preference_settings! {
history_sketches: HistorySketches = HistorySketches::default(), PAGE_LAYOUT;
/// A Play History button in the History controls, between Previous and Next (#3688). Off
/// gives the four controls History had before; right-clicking the controls toggles it too.
history_play_button: bool = true, PAGE_LAYOUT => check_id("prefs.history_play_button");
history_play_button: bool = true, PAGE_LAYOUT;
/// The vertical history (left, or under the browser) starts with the active component's Bodies
/// folder and the Origin, as the browser shows them (#6490). The bottom strip is unchanged.
timeline_origin_bodies: bool = false, PAGE_LAYOUT;
@@ -1332,10 +1332,10 @@ preference_settings! {
browser_bodies_folders: BodiesFolders = BodiesFolders::Several, PAGE_LAYOUT;
/// Each tree row's eye between its fold arrow and its radio, as before #8718, instead of in a
/// column at the rows' right end. Off by default.
browser_eye_left: bool = false, PAGE_LAYOUT => check_id("prefs.browser_eye_left");
browser_eye_left: bool = false, PAGE_LAYOUT;
/// Every component placement in the browser starts open, as before #9274, instead of closed
/// until the user opens it. Off by default.
browser_components_open: bool = false, PAGE_LAYOUT => check_id("prefs.browser_components_open");
browser_components_open: bool = false, PAGE_LAYOUT;
/// The wheel zooms toward the point under the cursor (else toward the view's centre).
zoom_to_cursor: bool = true, PAGE_NAVIGATION => check("Zoom to the cursor", "The wheel zooms toward the point under the cursor (off: toward the view's center)");
/// Speed multiplier for wheel zoom, independent of the 3D mouse.
@@ -4585,10 +4585,12 @@ mod tests {
navbar_click(&mut h, "prefs:navbar:slot:visual_style");
navbar_click(&mut h, "prefs:navbar:shows:0");
assert_eq!(h.app.preferences.navigation_bar.menu_display("visual_style"), MenuDisplay::IconAndValue);
navbar_click(&mut h, "prefs:navbar:position:0");
navbar_click(&mut h, "prefs:navbar:anchor:0");
assert_eq!(h.app.preferences.navigation_bar.position, Position::Left);
navbar_click(&mut h, "prefs:navbar:position:2");
assert_eq!(h.app.preferences.navigation_bar.edge, crate::navigation_bar::Edge::Top);
navbar_click(&mut h, "prefs:navbar:anchor:5");
assert_eq!(h.app.preferences.navigation_bar.position, Position::Right);
assert_eq!(h.app.preferences.navigation_bar.edge, crate::navigation_bar::Edge::Bottom);
navbar_click(&mut h, "prefs:navbar:size:0");
assert_eq!(h.app.preferences.navigation_bar.size, Size::Compact);
navbar_click(&mut h, "prefs:navbar:size:1");
@@ -4602,6 +4604,11 @@ mod tests {
let before = h.app.preferences.navigation_bar.clone();
navbar_click(&mut h, "prefs:navbar:preview_as:1");
assert_eq!(h.app.preferences.navigation_bar, before, "canvas preview is transient");
navbar_click(&mut h, "prefs:navbar:add:zoom");
assert_eq!(h.app.preferences.navigation_bar.slots.last().map(String::as_str), Some("zoom"));
let shared = h.app.preferences.navigation_bar.clone();
navbar_click(&mut h, "prefs:navbar:preview_as:0");
assert_eq!(h.app.preferences.navigation_bar, shared, "Render edits persist in the shared Design preview");
navbar_click(&mut h, "prefs:navbar:show");
assert!(!h.app.ui.show_navigation_bar);
navbar_click(&mut h, "prefs:navbar:show");
@@ -4621,11 +4628,16 @@ mod tests {
let search = crate::scenario::rect_of("prefs:search:frame").unwrap();
assert!(header.contains_rect(search), "titlebar search stays inside the window at {width}");
assert!(search.width() <= 230.0 && search.width() >= 100.0, "usable bounded search at {width}");
let body = crate::scenario::rect_of("prefs:page_body").unwrap();
for index in 0..6 {
let cell = crate::scenario::rect_of(&format!("prefs:navbar:anchor:{index}")).unwrap();
assert!(cell.left() >= body.left() && cell.right() <= body.right() + 0.5, "anchor {index} at {width}: {cell:?} outside {body:?}");
assert!(cell.width() > 20.0 && cell.height() > 20.0, "anchor {index} stays a usable target at {width}");
}
for (key, label, labels) in [
("position", "Position", &["Left", "Center", "Right"][..]),
("size", "Size", &["Compact", "Regular"][..]),
("buttons", "Buttons", &["Icons", "Icons and labels"][..]),
("idle", "When idle", &["Always visible", "Fade", "Hide until hovered"][..]),
("buttons", "Buttons", &["Icons", "Icons + labels"][..]),
("idle", "When idle", &["Always", "Fade", "Hover"][..]),
] {
let body = crate::scenario::rect_of("prefs:page_body").unwrap();
let row = crate::scenario::rect_of(&format!("prefs:navbar:{key}")).unwrap();
@@ -4633,20 +4645,8 @@ mod tests {
assert_eq!(row.height(), crate::ui_kit::row_height(), "standard control height");
let dropdown = crate::scenario::rect_of(&format!("dialog:segments:{label}")).is_none();
if !dropdown {
let t = crate::theme::Tokens::get();
let natural: f32 = labels
.iter()
.map(|option| {
h.ctx
.fonts_mut(|fonts| fonts.layout_no_wrap((*option).to_owned(), crate::fonts::semibold(t.font_label), t.form_ink))
.size()
.x
.ceil()
+ t.space_16
})
.sum();
let control = crate::scenario::rect_of(&format!("dialog:control:{label}")).unwrap();
assert!((row.width() - natural.min(control.width())).abs() < 0.5, "{key} hugs its content at {width}");
assert!((row.width() - control.width()).abs() < 0.5, "{key} fills its property column at {width}");
}
if dropdown {
// Standard Segments fall back to the kit dropdown when their labels cannot fit.
@@ -4733,15 +4733,27 @@ mod tests {
h.frames(3);
}
let body = crate::scenario::rect_of("prefs:page_body").unwrap();
let card = crate::scenario::rect_of("prefs:navbar:inspector").unwrap();
assert!(
(card.left() - body.left()).abs() < 0.5 && (card.right() - body.right()).abs() < 0.5,
"{slot:?} at {width}: {card:?} vs {body:?}"
);
if slot.is_some() {
let card = crate::scenario::rect_of("prefs:navbar:inspector").unwrap();
assert!(
(card.left() - body.left()).abs() < 0.5 && (card.right() - body.right()).abs() < 0.5,
"{slot:?} at {width}: {card:?} vs {body:?}"
);
} else {
assert!(crate::scenario::rect_of("prefs:navbar:inspector").is_none(), "inspector belongs to a selected slot");
}
let status = crate::scenario::rect_of("prefs:navbar:status").unwrap();
let reset = crate::scenario::rect_of("prefs:navbar:reset").unwrap();
let show = crate::scenario::rect_of("prefs:navbar:show").unwrap();
assert_eq!(status.right(), body.right());
assert!(status.left() >= body.left() && status.right() <= body.right() + 0.5, "header actions stay inside the page");
let stage = crate::scenario::rect_of("prefs:navbar:stage").unwrap();
let count = crate::scenario::rect_of("prefs:navbar:count").unwrap();
assert!(status.bottom() <= stage.top(), "Show and Reset belong above the shared stage");
let bar = crate::scenario::rect_of("prefs:navbar:bar").unwrap();
assert!(stage.contains_rect(count), "the real slot count sits in the stage");
assert!(!count.intersects(bar), "the count never covers the bar");
let properties = crate::scenario::rect_of("dialog:row:Position").unwrap();
assert!(stage.bottom() <= properties.top(), "the stage stays before properties");
assert!(status.contains_rect(reset) && status.contains_rect(show));
assert!(show.right() <= reset.left());
assert_eq!(reset.center().y, show.center().y);
@@ -4803,24 +4815,42 @@ mod tests {
}
#[test]
fn navigation_bar_preview_stage_follows_position_live() {
use crate::navigation_bar::Position;
fn navigation_bar_preview_stage_follows_anchor_live() {
use crate::navigation_bar::{Edge, Position};
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.call("ui.resize", json!({"width":1600,"height":1100}));
h.call("ui.prefs", json!({"open":"Interface/Navigation bar","navigation_bar":{"slots":["pan","fit","display"]}}));
for (index, position) in [(0, Position::Left), (1, Position::Centre), (2, Position::Right)] {
navbar_click(&mut h, &format!("prefs:navbar:position:{index}"));
for (index, edge, position) in [
(0, Edge::Top, Position::Left),
(1, Edge::Top, Position::Centre),
(2, Edge::Top, Position::Right),
(3, Edge::Bottom, Position::Left),
(4, Edge::Bottom, Position::Centre),
(5, Edge::Bottom, Position::Right),
] {
navbar_click(&mut h, &format!("prefs:navbar:anchor:{index}"));
h.frames(3);
let stage = crate::scenario::rect_of("prefs:navbar:stage").unwrap();
let bar = crate::scenario::rect_of("prefs:navbar:bar").unwrap();
assert_eq!(h.app.preferences.navigation_bar.position, position);
let count = crate::scenario::rect_of("prefs:navbar:count").unwrap();
assert_eq!((h.app.preferences.navigation_bar.edge, h.app.preferences.navigation_bar.position), (edge, position));
assert_eq!(stage.height(), crate::ui_kit::NAV_STAGE_HEIGHT);
assert_eq!(bar.center().y, stage.center().y);
let inset = crate::ui_kit::NAV_BORDER + crate::ui_kit::NAV_STAGE_INSET;
match edge {
Edge::Top => {
assert_eq!(bar.top(), stage.top() + inset);
assert!(count.top() >= bar.bottom(), "the count sits below a top bar");
}
Edge::Bottom => {
assert_eq!(bar.bottom(), stage.bottom() - inset);
assert!(count.bottom() <= bar.top(), "the count sits above a bottom bar");
}
}
match position {
Position::Left => assert_eq!(bar.left(), stage.left() + crate::ui_kit::NAV_BORDER + crate::ui_kit::NAV_STAGE_INSET),
Position::Left => assert_eq!(bar.left(), stage.left() + inset),
Position::Centre => assert_eq!(bar.center().x, stage.center().x),
Position::Right => assert_eq!(bar.right(), stage.right() - crate::ui_kit::NAV_BORDER - crate::ui_kit::NAV_STAGE_INSET),
Position::Right => assert_eq!(bar.right(), stage.right() - inset),
}
}
}
@@ -8756,6 +8786,49 @@ mod tests {
}
}
#[test]
fn layout_cards_and_toolbar_chips_preserve_transactions_in_both_themes() {
for theme in ["light", "dark"] {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.call("ui.resize", json!({"width":1200,"height":1100}));
h.call("ui.prefs", json!({"theme":theme,"quick_measure":true,"invert_curved_roll_arrows":true}));
h.call("ui.prefs", json!({"open":"Layout"}));
let before = h.app.preferences.clone();
let doc = h.app.session.doc.to_json();
for (key, left, under, browser) in
[("left", true, false, false), ("under_browser", true, true, false), ("browser", false, false, true), ("bottom", false, false, false)]
{
scroll_to_and_click(&mut h, &format!("prefs:history:{key}"));
assert_eq!(
(h.app.preferences.timeline_left, h.app.preferences.timeline_under_browser, h.app.preferences.history_in_browser),
(left, under, browser)
);
}
for (key, floating) in [("card", true), ("column", false), ("card", true)] {
scroll_to_and_click(&mut h, &format!("prefs:browser:{key}"));
assert_eq!(h.app.preferences.browser_float, floating);
}
for tab in crate::workspace::choosable_tabs() {
scroll_to_and_click(&mut h, &format!("prefs:toolbar_tab:{tab}"));
assert!(!crate::workspace::tab_turned_on(&h.app, tab));
scroll_to_and_click(&mut h, &format!("prefs:toolbar_tab:{tab}"));
assert!(crate::workspace::tab_turned_on(&h.app, tab));
}
assert_eq!(h.app.session.doc.to_json(), doc);
h.call("ui.prefs", json!({"button":"cancel"}));
assert_eq!(h.app.preferences, before, "Cancel restores every Layout edit");
h.call("ui.prefs", json!({"open":"Layout"}));
scroll_to_and_click(&mut h, "prefs:browser:card");
h.call("ui.prefs", json!({"button":"apply"}));
h.call("ui.prefs", json!({"button":"defaults"}));
assert!(!h.app.preferences.browser_float);
assert!(h.app.preferences.quick_measure && h.app.preferences.invert_curved_roll_arrows, "Layout reset retains Graphics and Navigation");
h.call("ui.prefs", json!({"button":"cancel"}));
assert!(h.app.preferences.browser_float, "Cancel after Apply returns to the applied state");
}
}
#[test]
fn history_preferences_widget_click_reveals_scrolled_content_and_keeps_folds_closed() {
let steps: serde_json::Value = serde_json::from_str(include_str!("../tests/scenarios/history_sketches_under_feature.json")).unwrap();
@@ -8774,7 +8847,7 @@ mod tests {
assert!(!page.contains(egui::pos2(at[0] as f32, at[1] as f32)), "the real control begins scrolled out of view");
assert!((at[1] as f32) < page.top(), "scrolling to the bottom puts the target above the viewport");
assert_eq!(h.app.preferences.history_sketches, HistorySketches::InOrder);
h.step(&json!({"widget":"Under their feature", "near":"Sketches"})).unwrap();
h.step(&json!({"widget":"Under their feature", "near":"Sketches in History"})).unwrap();
assert_eq!(h.app.preferences.history_sketches, HistorySketches::UnderFeature);
let at = h.widget("Under their feature").unwrap();
assert!(crate::scenario::rect_of("prefs:page_body").unwrap().contains(egui::pos2(at[0] as f32, at[1] as f32)));
+372 -230
View File
@@ -200,254 +200,396 @@ pub(super) fn appearance(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
pub(super) fn layout(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
ui_kit::settings_heading_with_tag(
ui,
app.tr("prefs.structure.layout"),
app.tr("prefs.structure.layout_note"),
Some(crate::settings_profiles::tag),
);
ui_kit::settings_section(ui, "layout", app.tr("prefs.layout"), |ui| {
ui_kit::settings_pair(
ui_kit::settings_page(ui, |ui| {
ui_kit::settings_heading_with_tag(
ui,
|ui| {
ui_kit::settings_form(ui, t.form_label_width + t.space_16 + t.space_6, |ui| {
ui_kit::row(ui, app.tr("prefs.workspace"), |ui| {
let mut selected = usize::from(app.ui.layout == crate::shell::LayoutMode::Docked);
if ui_kit::segmented(
ui,
&mut selected,
&[crate::i18n::tr("ui.layout_floating"), crate::i18n::tr("ui.layout_compact")],
Default::default(),
) {
let _ = app.run("view.layout", json!({"mode": if selected == 1 { "compact" } else { "floating" }}));
}
});
ui_kit::row(ui, app.tr("prefs.toolbar"), |ui| {
let mut selected = usize::from(!app.preferences.ribbon_labels);
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.labels"), app.tr("prefs.icons_only")], Default::default()) {
app.preferences.ribbon_labels = selected == 0;
}
});
ui_kit::row(ui, "", |ui| {
let check = setting::persistent_ribbon_pressed(app, ui).on_hover_text(app.tr("prefs.persistent_ribbon_pressed_help"));
crate::scenario::publish_target("prefs:persistent_ribbon_pressed", check.rect);
});
// The SKETCH tab outside a sketch too (#7904).
ui_kit::row(ui, "", |ui| {
let check = setting::sketch_tab_always(app, ui).on_hover_text(app.tr("prefs.sketch_tab_always_note"));
crate::scenario::publish_target("prefs:sketch_tab_always", check.rect);
});
// Each design tab keeps its dialog, tool and toolbar tab across a switch (#9360).
ui_kit::row(ui, "", |ui| {
let check = setting::keep_work_on_tab_switch(app, ui).on_hover_text(app.tr("prefs.keep_work_on_tab_switch_note"));
crate::scenario::publish_target("prefs:keep_work_on_tab_switch", check.rect);
});
// Search results point out their place in the ribbon (#7287).
ui_kit::row(ui, "", |ui| {
let check = setting::ribbon_reveal(app, ui).on_hover_text(app.tr("prefs.ribbon_reveal_note"));
crate::scenario::publish_target("prefs:ribbon_reveal", check.rect);
});
// Where every feature dialog puts its OK and Cancel (#6708).
ui_kit::row(ui, app.tr("prefs.dialog_buttons"), |ui| {
let placements = [crate::ui_kit::DialogButtons::Bottom, crate::ui_kit::DialogButtons::Top];
let mut selected = placements.iter().position(|p| *p == app.preferences.dialog_buttons).unwrap_or(0);
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.bottom"), app.tr("prefs.top")], Default::default())
&& let Some(placement) = placements.get(selected)
{
app.preferences.dialog_buttons = *placement;
}
});
// Handles that don't depend on the language, by the saved value.
let row = app.tr("prefs.dialog_buttons");
for (label, key) in [("prefs.bottom", "bottom"), ("prefs.top", "top")] {
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(label))) {
crate::scenario::publish_handle(&format!("prefs:dialog_buttons:{key}"), at);
}
}
// Where notices and offers stack (#7893).
ui_kit::row(ui, app.tr("prefs.toast_place"), |ui| {
let places = [crate::viewport::ToastPlace::Corner, crate::viewport::ToastPlace::Centre];
let mut selected = places.iter().position(|p| *p == app.preferences.toast_place).unwrap_or(0);
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.bottom_right"), app.tr("prefs.bottom_centre")], Default::default())
&& let Some(place) = places.get(selected)
{
app.preferences.toast_place = *place;
}
});
let row = app.tr("prefs.toast_place");
for (label, key) in [("prefs.bottom_right", "corner"), ("prefs.bottom_centre", "centre")] {
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(label))) {
crate::scenario::publish_handle(&format!("prefs:toast_place:{key}"), at);
}
}
// The ribbon's panel drop-downs: a list, or a grid of large icons (#4739).
ui_kit::row(ui, app.tr("prefs.menus"), |ui| {
let mut selected = usize::from(app.preferences.ribbon_icon_grid);
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.menu_list"), app.tr("prefs.menu_grid")], Default::default()) {
app.preferences.ribbon_icon_grid = selected == 1;
}
});
setting::ribbon_menus_skip_pinned(app, ui);
ui_kit::row(ui, app.tr("prefs.history"), |ui| {
let mut selected = if app.preferences.history_in_browser {
3
} else if app.preferences.timeline_left {
if app.preferences.timeline_under_browser { 2 } else { 1 }
} else {
0
};
if ui_kit::segmented(
ui,
&mut selected,
&[
crate::i18n::tr("prefs.bottom"),
crate::i18n::tr("prefs.left"),
crate::i18n::tr("prefs.under_browser"),
crate::i18n::tr("prefs.in_browser"),
],
ui_kit::Segments { proportional: true, compact: true, ..Default::default() },
) {
let dock = match selected {
1 => "left",
2 => "under_browser",
3 => "browser",
_ => "bottom",
};
let _ = app.run("view.history_layout", json!({ "dock": dock }));
}
});
for (short, legacy) in [
("prefs.bottom", "ui.timeline_bottom"),
("prefs.left", "ui.timeline_left_sidebar"),
("prefs.under_browser", "ui.history_below_browser"),
] {
let row = app.tr("prefs.history");
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(short))) {
crate::scenario::publish_handle(&format!("dialog:{row}:{}", app.tr(legacy)), at);
}
}
// The History at the left and the one in the browser can list a sketch under its
// feature (#6489); the timeline at the bottom can't.
ui_kit::row(ui, app.tr("prefs.history_sketches"), |ui| {
ui.add_enabled_ui(app.preferences.timeline_left || app.preferences.history_in_browser, |ui| {
let mut selected = usize::from(app.preferences.history_sketches == HistorySketches::UnderFeature);
app.tr("prefs.structure.layout"),
app.tr("prefs.structure.layout_note"),
Some(crate::settings_profiles::tag),
);
// Retain the section key for existing reveal/open callers.
ui_kit::settings_section(ui, "layout", app.tr("prefs.layout_page.window"), |ui| {
ui_kit::settings_pair(
ui,
|ui| {
ui_kit::settings_form(ui, t.form_label_width + t.space_16, |ui| {
ui_kit::row(ui, app.tr("prefs.workspace"), |ui| {
let mut selected = usize::from(app.ui.layout == crate::shell::LayoutMode::Docked);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.history_sketches_in_order"), app.tr("prefs.history_sketches_under_feature")],
ui_kit::Segments { proportional: true, compact: true, ..Default::default() },
&[crate::i18n::tr("ui.layout_floating"), crate::i18n::tr("ui.layout_compact")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) {
app.preferences.history_sketches =
if selected == 1 { HistorySketches::UnderFeature } else { HistorySketches::InOrder };
let _ = app.run("view.layout", json!({"mode": if selected == 1 { "compact" } else { "floating" }}));
}
});
});
// Only the history on the left or under the browser has room for these rows.
ui_kit::row(ui, "", |ui| {
ui.add_enabled_ui(app.preferences.timeline_left, |ui| {
let check = ui_kit::checkbox(ui, app.tr("prefs.history_origin_bodies"), &mut app.preferences.timeline_origin_bodies)
.on_hover_text(app.tr("prefs.history_origin_bodies_note"))
.on_disabled_hover_text(app.tr("prefs.history_origin_bodies_note"));
crate::scenario::publish_target("prefs:history_origin_bodies", check.rect);
});
});
// Play History between Previous and Next, for people who want it there (#3688).
ui_kit::row(ui, "", |ui| {
let check = setting::history_play_button(app, ui).on_hover_text(app.tr("prefs.history_play_button_note"));
crate::scenario::publish_target("prefs:history_play_button", check.rect);
});
ui_kit::row(ui, app.tr("prefs.browser"), |ui| {
ui.add_enabled_ui(app.ui.layout == crate::shell::LayoutMode::Floating, |ui| {
let mut selected = usize::from(app.preferences.browser_float);
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.opaque"), app.tr("prefs.translucent")], Default::default()) {
app.preferences.browser_float = selected == 1;
ui_kit::row(ui, app.tr("prefs.toolbar"), |ui| {
let mut selected = usize::from(!app.preferences.ribbon_labels);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.labels"), app.tr("prefs.icons_only")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) {
app.preferences.ribbon_labels = selected == 0;
}
});
});
// Out of sight until the pointer comes to it (#8692); the browser sharing its
// column with History keeps its place.
ui_kit::row(ui, "", |ui| {
use crate::browser::BrowserShow;
ui.add_enabled_ui(!crate::timeline_left::stacked(app), |ui| {
let mut selected = usize::from(app.preferences.browser_show == BrowserShow::Hover);
let options = [app.tr("prefs.browser_shown"), app.tr("prefs.browser_reveal_on_hover")];
let segments = ui.scope(|ui| ui_kit::segmented(ui, &mut selected, &options, Default::default()));
segments.response.on_hover_text(app.tr("prefs.browser_show_note"));
if segments.inner {
app.preferences.browser_show = if selected == 1 { BrowserShow::Hover } else { BrowserShow::Shown };
ui_kit::row(ui, app.tr("prefs.menus"), |ui| {
let mut selected = usize::from(app.preferences.ribbon_icon_grid);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.menu_list"), app.tr("prefs.menu_grid")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) {
app.preferences.ribbon_icon_grid = selected == 1;
}
});
});
// Where command dialogs open (#6707).
ui_kit::row(ui, app.tr("prefs.dialogs"), |ui| {
use crate::dialog_column::DialogPlace;
let mut selected = usize::from(app.preferences.dialog_place == DialogPlace::LeftColumn);
let options = [app.tr("prefs.dialogs_edge"), app.tr("prefs.dialogs_left_column")];
let segments = ui.scope(|ui| ui_kit::segmented(ui, &mut selected, &options, Default::default()));
segments.response.on_hover_text(app.tr("prefs.dialogs_note"));
if segments.inner {
app.preferences.dialog_place = if selected == 1 { DialogPlace::LeftColumn } else { DialogPlace::Edge };
ui_kit::row(ui, app.tr("prefs.toast_place"), |ui| {
let places = [crate::viewport::ToastPlace::Corner, crate::viewport::ToastPlace::Centre];
let mut selected = places.iter().position(|p| *p == app.preferences.toast_place).unwrap_or(0);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.bottom_right"), app.tr("prefs.bottom_centre")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) && let Some(place) = places.get(selected)
{
app.preferences.toast_place = *place;
}
});
let row = app.tr("prefs.toast_place");
for (label, key) in [("prefs.bottom_right", "corner"), ("prefs.bottom_centre", "centre")] {
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(label))) {
crate::scenario::publish_handle(&format!("prefs:toast_place:{key}"), at);
}
}
layout_check(ui, app.tr("prefs.status_bar"), app.tr("prefs.status_bar_note"), &mut app.preferences.status_bar, "status_bar");
});
// Where a dialog's and a sketch's OK and Cancel also sit (#6709).
ui_kit::row(ui, "", |ui| {
let check = setting::view_confirm_corner(app, ui).on_hover_text(app.tr("prefs.view_confirm_corner_note"));
crate::scenario::publish_target("prefs:view_confirm_corner", check.rect);
});
ui_kit::row(ui, app.tr("prefs.bodies_folders"), |ui| {
let modes = BodiesFolders::ALL;
let labels: Vec<_> = modes.iter().map(|m| app.tr(m.label_id())).collect();
let mut selected = modes.iter().position(|m| *m == app.preferences.browser_bodies_folders).unwrap_or(0);
if ui_kit::segmented(ui, &mut selected, &labels, ui_kit::Segments { proportional: true, compact: true, ..Default::default() })
&& let Some(mode) = modes.get(selected)
{
app.preferences.browser_bodies_folders = *mode;
}
});
// Placements open in the browser from the start, as before #9274.
ui_kit::row(ui, "", |ui| {
let check = setting::browser_components_open(app, ui).on_hover_text(app.tr("prefs.browser_components_open_note"));
crate::scenario::publish_target("prefs:browser_components_open", check.rect);
});
// The eye beside the fold arrow, where it was before #8718.
ui_kit::row(ui, "", |ui| {
let check = setting::browser_eye_left(app, ui).on_hover_text(app.tr("prefs.browser_eye_left_note"));
crate::scenario::publish_target("prefs:browser_eye_left", check.rect);
});
// What is being edited, the sketch's state and the units, along the bottom (#7243).
ui_kit::row(ui, "", |ui| {
let check = setting::status_bar(app, ui).on_hover_text(app.tr("prefs.status_bar_note"));
crate::scenario::publish_target("prefs:status_bar", check.rect);
});
ui_kit::text(ui, app.tr("prefs.layout_note"), ui_kit::Text::Hint("layout"));
ui_kit::text(ui, app.tr("prefs.history_sketches_note"), ui_kit::Text::Hint("history_sketches"));
ui_kit::LayoutPreview {
compact: app.ui.layout == crate::shell::LayoutMode::Docked,
labels: app.preferences.ribbon_labels,
history: if app.preferences.history_in_browser {
3
} else if app.preferences.timeline_left {
if app.preferences.timeline_under_browser { 2 } else { 1 }
} else {
0
},
history: history_choice(app),
translucent: app.preferences.browser_float,
dialog_left: app.preferences.dialog_place == crate::dialog_column::DialogPlace::LeftColumn,
}
})
},
ui_kit::layout_preview,
);
});
super::navbar::show(app, ui);
// The same choice as the toolbar's own menu (the plus after the tabs, or right-click a tab).
ui_kit::settings_section(ui, "toolbar_tabs", app.tr("prefs.toolbar_tabs"), |ui| {
for tab in crate::workspace::choosable_tabs() {
let mut on = crate::workspace::tab_turned_on(app, tab);
let response = ui_kit::checkbox(ui, &crate::toolbar::ribbon_label(app, tab), &mut on);
crate::scenario::publish_target(&format!("prefs:toolbar_tab:{tab}"), response.rect);
if response.changed() {
crate::workspace::set_tab_shown(app, tab, on);
}
},
ui_kit::layout_preview,
);
layout_check(
ui,
app.tr("prefs.keep_work_on_tab_switch"),
app.tr("prefs.keep_work_on_tab_switch_note"),
&mut app.preferences.keep_work_on_tab_switch,
"keep_work_on_tab_switch",
);
});
if let Some(rect) = crate::scenario::rect_of(&format!("dialog:section:{}", app.tr("prefs.layout_page.window"))) {
crate::scenario::publish_target(&format!("dialog:section:{}", app.tr("prefs.layout")), rect);
}
ui_kit::settings_section(ui, "panels", app.tr("prefs.layout_page.panels"), |ui| {
let options = [
ui_kit::PictureChoice { key: "bottom", label: app.tr("prefs.bottom"), figure: ui_kit::paint_history_bottom },
ui_kit::PictureChoice { key: "left", label: app.tr("prefs.left"), figure: ui_kit::paint_history_left },
ui_kit::PictureChoice { key: "under_browser", label: app.tr("prefs.under_browser"), figure: ui_kit::paint_history_under_browser },
ui_kit::PictureChoice { key: "browser", label: app.tr("prefs.in_browser"), figure: ui_kit::paint_history_in_browser },
];
if let Some(selected) = ui_kit::settings_picture_choices(ui, "history", app.tr("prefs.history"), &options, history_choice(app), true)
&& let Some(option) = options.get(selected)
{
let _ = app.run("view.history_layout", json!({"dock":option.key}));
}
for (short, legacy) in [
("prefs.bottom", "ui.timeline_bottom"),
("prefs.left", "ui.timeline_left_sidebar"),
("prefs.under_browser", "ui.history_below_browser"),
] {
let row = app.tr("prefs.history");
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(short))) {
crate::scenario::publish_handle(&format!("dialog:{row}:{}", app.tr(legacy)), at);
}
}
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = t.space_16;
let cards_width = (t.space_24 * 10.0 + t.space_2).min(ui.max_rect().width());
ui.allocate_ui_with_layout(egui::vec2(cards_width, 0.0), egui::Layout::top_down(egui::Align::Min), |ui| {
let options = [
ui_kit::PictureChoice { key: "column", label: app.tr("prefs.layout_page.column"), figure: ui_kit::paint_browser_column },
ui_kit::PictureChoice { key: "card", label: app.tr("prefs.layout_page.card"), figure: ui_kit::paint_browser_card },
];
if let Some(selected) = ui_kit::settings_picture_choices(
ui,
"browser",
app.tr("prefs.browser"),
&options,
usize::from(app.preferences.browser_float),
app.ui.layout == crate::shell::LayoutMode::Floating,
) {
app.preferences.browser_float = selected == 1;
}
for (key, legacy) in [("column", "prefs.opaque"), ("card", "prefs.translucent")] {
if let Some(at) = crate::scenario::handle_at(&format!("prefs:browser:{key}")) {
crate::scenario::publish_handle(&format!("dialog:{}:{}", app.tr("prefs.browser"), app.tr(legacy)), at);
}
}
});
let labels = [app.tr("prefs.browser_shown"), app.tr("prefs.layout_page.on_hover")];
let width = labels.iter().map(|label| ui_kit::ContentWidth::name(ui, label)).fold(0.0, f32::max) * 2.0 + t.space_16 * 3.0 + t.space_6;
ui.allocate_ui_with_layout(
egui::vec2(width.min(ui.max_rect().width()), t.control_height),
egui::Layout::top_down(egui::Align::Min),
|ui| {
browser_visibility(app, ui);
},
);
});
ui_kit::settings_form(ui, t.form_label_width + t.space_24 * 3.0, |ui| {
ui_kit::settings_row_help(ui, app.tr("prefs.history_sketches"), app.tr("prefs.history_sketches_note"), |ui| {
ui.add_enabled_ui(app.preferences.timeline_left || app.preferences.history_in_browser, |ui| {
let mut selected = usize::from(app.preferences.history_sketches == HistorySketches::UnderFeature);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.history_sketches_in_order"), app.tr("prefs.history_sketches_under_feature")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) {
app.preferences.history_sketches = if selected == 1 { HistorySketches::UnderFeature } else { HistorySketches::InOrder };
}
});
});
for (key, label) in [("in_order", "prefs.history_sketches_in_order"), ("under_feature", "prefs.history_sketches_under_feature")] {
if let Some(rect) = crate::scenario::rect_of(&format!("dialog:{}:{}", app.tr("prefs.history_sketches"), app.tr(label))) {
crate::scenario::publish_target(&format!("prefs:history_sketches:{key}"), rect);
crate::scenario::publish_target(&format!("dialog:Sketches:{}", app.tr(label)), rect);
}
}
ui_kit::row(ui, app.tr("prefs.bodies_folders"), |ui| {
let modes = BodiesFolders::ALL;
let labels: Vec<_> = modes.iter().map(|m| app.tr(m.label_id())).collect();
let mut selected = modes.iter().position(|m| *m == app.preferences.browser_bodies_folders).unwrap_or(0);
if ui_kit::segmented(ui, &mut selected, &labels, ui_kit::Segments { intrinsic: true, ..Default::default() })
&& let Some(mode) = modes.get(selected)
{
app.preferences.browser_bodies_folders = *mode;
}
if let Some(rect) =
crate::scenario::rect_of(&format!("dialog:{}:{}", app.tr("prefs.bodies_folders"), app.tr("prefs.bodies_folders_several")))
{
crate::scenario::publish_target(&format!("dialog:{}:Several bodies only", app.tr("prefs.bodies_folders")), rect);
}
});
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(t.space_16, t.space_6);
layout_check(
ui,
app.tr("prefs.layout_page.play_history"),
app.tr("prefs.history_play_button_note"),
&mut app.preferences.history_play_button,
"history_play_button",
);
ui.add_enabled_ui(app.preferences.timeline_left, |ui| {
layout_check(
ui,
app.tr("prefs.layout_page.origin_bodies"),
app.tr("prefs.history_origin_bodies_note"),
&mut app.preferences.timeline_origin_bodies,
"history_origin_bodies",
);
});
layout_check(
ui,
app.tr("prefs.browser_components_open"),
app.tr("prefs.browser_components_open_note"),
&mut app.preferences.browser_components_open,
"browser_components_open",
);
layout_check(
ui,
app.tr("prefs.browser_eye_left"),
app.tr("prefs.browser_eye_left_note"),
&mut app.preferences.browser_eye_left,
"browser_eye_left",
);
});
});
ui_kit::settings_section(ui, "dialogs", app.tr("prefs.dialogs"), |ui| {
ui_kit::settings_form(ui, t.form_label_width + t.space_24, |ui| {
ui_kit::settings_row_help(ui, app.tr("prefs.layout_page.open"), app.tr("prefs.dialogs_note"), |ui| {
use crate::dialog_column::DialogPlace;
let mut selected = usize::from(app.preferences.dialog_place == DialogPlace::LeftColumn);
let options = [app.tr("prefs.dialogs_edge"), app.tr("prefs.dialogs_left_column")];
let segments =
ui.scope(|ui| ui_kit::segmented(ui, &mut selected, &options, ui_kit::Segments { intrinsic: true, ..Default::default() }));
segments.response.on_hover_text(app.tr("prefs.dialogs_note"));
if segments.inner {
app.preferences.dialog_place = if selected == 1 { DialogPlace::LeftColumn } else { DialogPlace::Edge };
}
});
for option in ["prefs.dialogs_edge", "prefs.dialogs_left_column"] {
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{}:{}", app.tr("prefs.layout_page.open"), app.tr(option))) {
crate::scenario::publish_handle(&format!("dialog:{}:{}", app.tr("prefs.dialogs"), app.tr(option)), at);
}
}
ui_kit::row(ui, app.tr("prefs.layout_page.ok_cancel"), |ui| {
let placements = [crate::ui_kit::DialogButtons::Bottom, crate::ui_kit::DialogButtons::Top];
let mut selected = placements.iter().position(|p| *p == app.preferences.dialog_buttons).unwrap_or(0);
if ui_kit::segmented(
ui,
&mut selected,
&[app.tr("prefs.bottom"), app.tr("prefs.top")],
ui_kit::Segments { intrinsic: true, ..Default::default() },
) && let Some(placement) = placements.get(selected)
{
app.preferences.dialog_buttons = *placement;
}
});
// Handles that don't depend on the language, by the saved value.
let row = app.tr("prefs.layout_page.ok_cancel");
for (label, key) in [("prefs.bottom", "bottom"), ("prefs.top", "top")] {
if let Some(at) = crate::scenario::handle_at(&format!("dialog:{row}:{}", app.tr(label))) {
crate::scenario::publish_handle(&format!("prefs:dialog_buttons:{key}"), at);
crate::scenario::publish_handle(&format!("dialog:{}:{}", app.tr("prefs.dialog_buttons"), app.tr(label)), at);
}
}
});
layout_check(
ui,
app.tr("prefs.layout_page.view_corner"),
app.tr("prefs.view_confirm_corner_note"),
&mut app.preferences.view_confirm_corner,
"view_confirm_corner",
);
});
ui_kit::settings_section(ui, "toolbar_tabs", app.tr("prefs.toolbar_tabs"), |ui| {
ui.horizontal_wrapped(|ui| {
for tab in crate::workspace::choosable_tabs() {
let label = crate::toolbar::ribbon_label(app, tab);
let on = crate::workspace::tab_turned_on(app, tab);
if ui_kit::settings_tab_chip(ui, tab, &label, crate::workspace::tab_icon(tab), on) {
crate::workspace::set_tab_shown(app, tab, !on);
}
}
});
let labels = [
app.tr("prefs.sketch_tab_always"),
app.tr("prefs.layout_page.active_commands"),
app.tr("prefs.ribbon_reveal"),
app.tr("prefs.layout_page.pinned_menus"),
];
let widest =
labels.iter().map(|label| ui_kit::ContentWidth::label(ui, label)).fold(0.0, f32::max) + t.space_14 + t.space_6 * 2.0 + t.space_16;
let columns = if ui.available_width() >= widest * 2.0 + t.space_16 { 2 } else { 1 };
let width = (ui.available_width() - t.space_16 * (columns - 1) as f32) / columns as f32;
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = t.space_6;
for row in (0..4).step_by(columns) {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = t.space_16;
for index in row..(row + columns).min(4) {
ui.allocate_ui_with_layout(
egui::vec2(width.max(0.0), t.control_height),
egui::Layout::top_down(egui::Align::Min),
|ui| match index {
0 => {
layout_check(
ui,
app.tr("prefs.sketch_tab_always"),
app.tr("prefs.sketch_tab_always_note"),
&mut app.preferences.sketch_tab_always,
"sketch_tab_always",
);
}
1 => {
layout_check(
ui,
app.tr("prefs.layout_page.active_commands"),
app.tr("prefs.persistent_ribbon_pressed_help"),
&mut app.preferences.persistent_ribbon_pressed,
"persistent_ribbon_pressed",
);
}
2 => {
layout_check(
ui,
app.tr("prefs.ribbon_reveal"),
app.tr("prefs.ribbon_reveal_note"),
&mut app.preferences.ribbon_reveal,
"ribbon_reveal",
);
}
3 => {
layout_check(
ui,
app.tr("prefs.layout_page.pinned_menus"),
app.tr("ui.ribbon_menus_skip_pinned"),
&mut app.preferences.ribbon_menus_skip_pinned,
"ribbon_menus_skip_pinned",
);
}
_ => {}
},
);
}
});
}
});
});
super::navbar::show(app, ui);
});
}
fn history_choice(app: &SolveApp) -> usize {
if app.preferences.history_in_browser {
3
} else if app.preferences.timeline_left {
if app.preferences.timeline_under_browser { 2 } else { 1 }
} else {
0
}
}
/// Keep native checkbox responses and one real search target; guidance stays on hover.
fn layout_check(ui: &mut egui::Ui, label: &str, help: &str, value: &mut bool, key: &str) {
let t = Tokens::get();
let width = (ui_kit::ContentWidth::label(ui, label) + t.space_14 + t.space_6 * 2.0 + t.space_16).min(ui.max_rect().width());
ui.allocate_ui_with_layout(egui::vec2(width.max(0.0), t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| {
ui_kit::settings_form(ui, 0.0, |ui| {
ui_kit::row(ui, "", |ui| {
ui.spacing_mut().item_spacing.x = t.space_6;
let check_width = (ui.available_width() - t.space_16 - t.space_6).max(0.0);
let response = ui_kit::settings_control_width(ui, check_width, |ui| {
ui_kit::checkbox(ui, label, value).on_hover_text(help).on_disabled_hover_text(help)
});
crate::scenario::publish_target(&format!("prefs:{key}"), response.rect);
ui_kit::help_icon(ui, help);
})
});
});
}
fn browser_visibility(app: &mut SolveApp, ui: &mut egui::Ui) {
use crate::browser::BrowserShow;
ui_kit::settings_form(ui, 0.0, |ui| {
ui_kit::row(ui, app.tr("prefs.browser_shown"), |ui| {
ui.spacing_mut().item_spacing.x = Tokens::get().space_6;
crate::prefs_search::note_current(ui, app.tr("prefs.layout_page.on_hover"));
ui.add_enabled_ui(!crate::timeline_left::stacked(app), |ui| {
let mut selected = usize::from(app.preferences.browser_show == BrowserShow::Hover);
if ui_kit::settings_segments(
ui,
app.tr("prefs.browser_shown"),
&mut selected,
&[app.tr("prefs.browser_shown"), app.tr("prefs.layout_page.on_hover")],
) {
app.preferences.browser_show = if selected == 1 { BrowserShow::Hover } else { BrowserShow::Shown };
}
if let Some(rect) = crate::scenario::rect_of(&format!("dialog:segments:{}", app.tr("prefs.browser_shown"))) {
crate::scenario::publish_target("prefs:browser_show", rect);
}
ui_kit::help_icon(ui, app.tr("prefs.browser_show_note"));
});
});
});
}
+256 -102
View File
@@ -23,18 +23,6 @@ struct Source {
type Drag = ui_kit::ReorderPayload<Source>;
pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
// The bar editor is drawn by hand, not through the kit rows the search hides, so while a
// search runs it stays out unless the section's own title or a setting's label matches.
if crate::prefs_search::querying(ui) {
let labels: Vec<String> = ["title", "preview_as", "position", "size", "buttons", "idle", "show", "reset_default"]
.iter()
.map(|key| app.tr(&format!("navbar.custom.{key}")).to_owned())
.collect();
let texts: Vec<&str> = labels.iter().map(String::as_str).collect();
if !crate::prefs_search::texts_hit(ui, &texts) {
return;
}
}
let scroll = app.prefs_window.navbar_scroll;
let mut state = std::mem::take(&mut app.prefs_window.navbar);
if state.slots != app.preferences.navigation_bar.slots {
@@ -47,24 +35,49 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
}
ui_kit::settings_section(ui, "navigation_bar", crate::i18n::tr("navbar.custom.title"), |ui| {
let width = ui.available_width();
ui_kit::text(ui, app.tr("navbar.custom.intro"), ui_kit::Text::Hint("navbar_intro"));
segments(ui, "preview_as", &mut usize::from(state.render), &["navbar.custom.design", "navbar.custom.render"], |i| state.render = i == 1);
preview(app, ui, &mut state, width);
let card = ui_kit::settings_card(ui, |ui| inspector(app, ui, &mut state));
scenario::publish_rect("prefs:navbar:inspector", card.response.rect);
available(app, ui, &mut state);
let t = Tokens::get();
let header = ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = t.space_16;
let label_width = ui_kit::ContentWidth::label(ui, app.tr("navbar.custom.preview_as")).max(t.form_label_width);
let tabs_width = [app.tr("navbar.custom.design"), app.tr("navbar.custom.render")]
.iter()
.map(|label| ui_kit::ContentWidth::name(ui, label) + t.space_16)
.sum::<f32>();
let preview_width = (label_width + t.space_14 + tabs_width + t.space_16 + t.space_6).min(width);
ui.allocate_ui_with_layout(vec2(preview_width, t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| {
ui_kit::settings_form(ui, label_width, |ui| {
segments(ui, "preview_as", &mut usize::from(state.render), &["navbar.custom.design", "navbar.custom.render"], |i| {
state.render = i == 1
});
});
});
let actions_minimum = ui_kit::ContentWidth::label(ui, app.tr("navbar.custom.show"))
+ t.space_14
+ t.space_6
+ t.space_8
+ ui_kit::ContentWidth::button(ui, app.tr("navbar.custom.reset_default"));
let actions_width = (width - preview_width - t.space_16).max(actions_minimum).min(width);
ui.allocate_ui_with_layout(vec2(actions_width, t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| {
header_actions(app, ui, &mut state);
});
});
scenario::publish_rect("prefs:navbar:header", header.response.rect);
if !crate::prefs_search::suppress_chrome(ui) {
preview(app, ui, &mut state, width);
if state.selected.is_some() {
let card = ui_kit::settings_card(ui, |ui| inspector(app, ui, &mut state));
scenario::publish_rect("prefs:navbar:inspector", card.response.rect);
}
available(app, ui, &mut state);
}
ui_kit::divider(ui, "navbar_appearance");
appearance(app, ui);
ui_kit::settings_inline_note(
ui,
&crate::i18n::format("navbar.custom.count", &[("count", &app.preferences.navigation_bar.slots.len().to_string())]),
);
if let Some(message) =
state.notice.or_else(|| (app.preferences.navigation_bar.slots.len() >= navigation_bar::MAX_SLOTS).then_some("navbar.custom.full"))
{
ui.label(egui::RichText::new(app.tr(message)).color(Tokens::get().warning));
}
footer(app, ui, &mut state);
});
if let Some(rect) = scenario::rect_of(&format!("dialog:section:{}", app.tr("navbar.custom.title"))) {
scenario::publish_rect("prefs:section:navigation_bar", rect);
@@ -78,28 +91,55 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
app.prefs_window.navbar = state;
}
fn segments(ui: &mut egui::Ui, key: &str, selected: &mut usize, options: &[&str], mut change: impl FnMut(usize)) {
fn segments(ui: &mut egui::Ui, key: &str, selected: &mut usize, options: &[&str], change: impl FnMut(usize)) {
let label_key = format!("navbar.custom.{key}");
let label = crate::i18n::tr(&label_key);
let labels: Vec<_> = options.iter().map(|key| crate::i18n::tr(key)).collect();
ui_kit::row(ui, label, |ui| {
if ui_kit::segmented(ui, selected, &labels, ui_kit::Segments { intrinsic: true, ..Default::default() }) {
change(*selected);
}
if let Some(rect) = scenario::rect_of(&format!("dialog:segments:{label}")) {
scenario::publish_target(&format!("prefs:navbar:{key}"), rect);
} else if let Some(rect) = scenario::rect_of(&format!("dialog:widget:{label}")) {
scenario::publish_target(&format!("prefs:navbar:{key}"), rect);
}
for (index, option) in labels.iter().enumerate() {
if let Some(rect) = scenario::rect_of(&format!("dialog:{label}:{option}")) {
scenario::publish_target(&format!("prefs:navbar:{key}:{index}"), rect);
}
}
});
ui_kit::row(ui, label, |ui| segment_control(ui, key, selected, options, change));
}
fn footer(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
fn segment_control(ui: &mut egui::Ui, key: &str, selected: &mut usize, options: &[&str], mut change: impl FnMut(usize)) {
ui.spacing_mut().item_spacing.x = Tokens::get().space_6;
let label_key = format!("navbar.custom.{key}");
let label = crate::i18n::tr(&label_key);
let labels: Vec<_> = options
.iter()
.map(|key| {
crate::i18n::tr(match *key {
"navbar.custom.always" => "prefs.layout_page.always",
"navbar.custom.hide_idle" => "prefs.layout_page.hover",
"navbar.custom.labels" => "prefs.layout_page.icons_labels",
_ => key,
})
})
.collect();
if ui_kit::segmented(ui, selected, &labels, ui_kit::Segments { intrinsic: matches!(key, "preview_as" | "shows"), ..Default::default() }) {
change(*selected);
}
if key == "preview_as" {
ui_kit::help_icon(ui, crate::i18n::tr("navbar.custom.intro"));
}
if let Some(rect) = scenario::rect_of(&format!("dialog:segments:{label}")) {
scenario::publish_target(&format!("prefs:navbar:{key}"), rect);
} else if let Some(rect) = scenario::rect_of(&format!("dialog:widget:{label}")) {
scenario::publish_target(&format!("prefs:navbar:{key}"), rect);
}
for (index, option) in labels.iter().enumerate() {
if let Some(rect) = scenario::rect_of(&format!("dialog:{label}:{option}")) {
scenario::publish_target(&format!("prefs:navbar:{key}:{index}"), rect);
if let Some(legacy) = options.get(index) {
scenario::publish_target(&format!("dialog:{label}:{}", crate::i18n::tr(legacy)), rect);
}
}
}
}
fn option(ui: &mut egui::Ui, key: &str, selected: &mut usize, options: &[&str], change: impl FnMut(usize)) {
let label_key = format!("navbar.custom.{key}");
let label = crate::i18n::tr(&label_key);
ui_kit::settings_compound_field(ui, label, label, |ui| segment_control(ui, key, selected, options, change));
}
fn header_actions(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
let t = Tokens::get();
let label = app.tr("navbar.custom.reset_default");
let width = ui_kit::ContentWidth::button(ui, label).max(t.control_height);
@@ -107,14 +147,22 @@ fn footer(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
let reset = Rect::from_min_max(egui::pos2((rect.right() - width).max(rect.left()), rect.top()), rect.max);
let check = Rect::from_min_max(rect.min, egui::pos2((reset.left() - t.space_8).max(rect.left()), rect.bottom()));
ui.scope_builder(egui::UiBuilder::new().max_rect(check).layout(egui::Layout::left_to_right(egui::Align::Center)), |ui| {
let mut visible = app.ui.show_navigation_bar;
let response = ui_kit::checkbox(ui, app.tr("navbar.custom.show"), &mut visible);
scenario::publish_target("prefs:navbar:show", response.rect);
if response.changed() {
let _ = app.run("view.navigation_bar", json!({"visible":visible}));
}
ui_kit::settings_form(ui, 0.0, |ui| {
ui_kit::row(ui, "", |ui| {
let mut visible = app.ui.show_navigation_bar;
let response = ui_kit::checkbox(ui, app.tr("navbar.custom.show"), &mut visible);
scenario::publish_target("prefs:navbar:show", response.rect);
if response.changed() {
let _ = app.run("view.navigation_bar", json!({"visible":visible}));
}
})
});
});
let response = ui_kit::button_at(ui, "navbar_reset", reset, label, ui_kit::Button::Normal);
let response = ui
.scope_builder(egui::UiBuilder::new().max_rect(reset), |ui| {
ui_kit::settings_form(ui, 0.0, |ui| ui_kit::row(ui, label, |ui| ui_kit::button(ui, label, ui_kit::Button::Normal)))
})
.inner;
scenario::publish_target("prefs:navbar:reset", response.rect);
scenario::publish_rect("prefs:navbar:status", rect);
if response.clicked() {
@@ -166,15 +214,42 @@ fn preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, width: f32)
});
}
}
slot_count(app, ui, stage, bar.rect);
let target = if egui::DragAndDrop::payload::<Drag>(ui.ctx()).is_some_and(|payload| payload.id.index.is_some()) {
bar.rect.intersect(stage)
} else {
stage
};
drop_on_preview(app, ui, state, target.intersect(ui.clip_rect()), &cells);
drop_on_preview(app, ui, state, target.intersect(ui.clip_rect()), &cells, bar.rect.center().y);
}
fn drop_on_preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, target: Rect, cells: &[(usize, Rect)]) {
/// The slot count sits in the stage on the side of the bar facing the middle of the view, aligned with the bar.
fn slot_count(app: &SolveApp, ui: &mut egui::Ui, stage: Rect, bar: Rect) {
let t = Tokens::get();
let view = Tokens::view();
let text = crate::i18n::format(
"prefs.layout_page.slot_count",
&[("count", &app.preferences.navigation_bar.slots.len().to_string()), ("max", &navigation_bar::MAX_SLOTS.to_string())],
);
let galley = ui.painter().layout_no_wrap(text.clone(), egui::FontId::proportional(t.font_body), view.text_dim);
let size = galley.size();
let x = match app.preferences.navigation_bar.position {
Position::Left => stage.left() + ui_kit::NAV_BORDER + ui_kit::NAV_STAGE_INSET + ui_kit::NAV_PADDING,
Position::Centre => stage.center().x - size.x / 2.0,
Position::Right => stage.right() - ui_kit::NAV_BORDER - ui_kit::NAV_STAGE_INSET - ui_kit::NAV_PADDING - size.x,
};
let y = match app.preferences.navigation_bar.edge {
Edge::Top => bar.bottom() + t.space_8,
Edge::Bottom => bar.top() - t.space_8 - size.y,
};
let rect = Rect::from_min_size(egui::pos2(x, y), size);
ui.painter().with_clip_rect(stage).galley(rect.min, galley, view.text_dim);
let response = ui.interact(rect, ui.id().with("navbar_slot_count"), Sense::hover());
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), text.as_str()));
scenario::publish_rect("prefs:navbar:count", rect);
}
fn drop_on_preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, target: Rect, cells: &[(usize, Rect)], marker_y: f32) {
let Some(payload) = egui::DragAndDrop::payload::<Drag>(ui.ctx()) else { return };
let Some(pointer) = ui.input(|i| i.pointer.interact_pos()) else { return };
let over = target.contains(pointer) && ui.rect_contains_pointer(target);
@@ -189,8 +264,8 @@ fn drop_on_preview(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, tar
let marker = ui_kit::insertion_marker_with_ink(
ui.painter(),
x,
target.center().y - ui_kit::NAV_MARKER_HEIGHT / 2.0,
target.center().y + ui_kit::NAV_MARKER_HEIGHT / 2.0,
marker_y - ui_kit::NAV_MARKER_HEIGHT / 2.0,
marker_y + ui_kit::NAV_MARKER_HEIGHT / 2.0,
Tokens::get().form_primary,
);
scenario::publish_target("prefs:navbar:marker", marker);
@@ -239,11 +314,9 @@ fn inspector(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
.selected
.and_then(|index| app.preferences.navigation_bar.slots.get(index).and_then(|id| navigation_bar::find(id)).map(|slot| (index, slot)))
else {
ui_kit::text(ui, app.tr("navbar.custom.select"), ui_kit::Text::Hint("navbar_select"));
return;
};
inspector_header(app, ui, state, index, slot);
ui_kit::text(ui, app.tr(slot.description), ui_kit::Text::Hint("navbar_description"));
if slot.id == "visual_style" {
let mut selected = usize::from(app.preferences.navigation_bar.menu_display(slot.id) == MenuDisplay::IconOnly);
segments(ui, "shows", &mut selected, &["navbar.custom.icon_value", "navbar.custom.icon_only"], |index| {
@@ -290,6 +363,7 @@ fn inspector_header(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State, in
}
ui_kit::text(ui, app.tr(slot.label), ui_kit::Text::NameCell(true));
});
ui.interact(name, ui.make_persistent_id("slot_help"), Sense::hover()).on_hover_text(app.tr(slot.description));
scenario::publish_rect("prefs:navbar:inspector:name", name);
scenario::publish_rect("prefs:navbar:inspector:header", bounds);
let mut actions_ui = ui.new_child(egui::UiBuilder::new().id_salt("slot_actions").max_rect(bounds));
@@ -330,15 +404,44 @@ fn available(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
ui.scope(|ui| {
ui.spacing_mut().item_spacing.y = Tokens::get().space_6;
let caption = crate::i18n::format("navbar.custom.available", &[("available", app.tr("ui.workspace_available"))]);
ui_kit::text(ui, &caption, ui_kit::Text::Hint("navbar_available"));
ui.horizontal(|ui| {
ui_kit::settings_inline_note(ui, app.tr("ui.workspace_available"));
ui_kit::help_icon(ui, &caption);
// Separators are not buttons: their source sits apart, at the end of the heading.
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some(slot) = navigation_bar::find("separator") {
let hint = app.tr("navbar.custom.separator_hint");
let dragging =
egui::DragAndDrop::payload::<Drag>(ui.ctx()).is_some_and(|payload| payload.from_menu && payload.id.slot == slot.id);
let (tile, add) = ui_kit::navigation_separator_source(ui, slot.id, app.tr(slot.label), dragging);
let tile = tile.on_hover_text(hint);
scenario::publish_target(&format!("prefs:navbar:tile:{}", slot.id), tile.rect);
scenario::publish_target(&format!("prefs:navbar:add:{}", slot.id), add.rect);
if tile.drag_started() {
tile.dnd_set_drag_payload(Drag {
id: Source { slot: slot.id.to_owned(), index: None, slots: app.preferences.navigation_bar.slots.clone() },
from_menu: true,
});
}
if add.clicked() {
let index = app.preferences.navigation_bar.slots.len();
state.notice = app.preferences.navigation_bar.insert(index, slot.id).err();
if state.notice.is_none() {
state.selected = Some(index);
}
}
}
});
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = vec2(Tokens::get().space_8, Tokens::get().space_8);
for slot in navigation_bar::CATALOGUE {
if slot.kind != Kind::Separator && app.preferences.navigation_bar.slots.iter().any(|id| id == slot.id) {
if slot.kind == Kind::Separator || app.preferences.navigation_bar.slots.iter().any(|id| id == slot.id) {
continue;
}
let dragging = egui::DragAndDrop::payload::<Drag>(ui.ctx()).is_some_and(|payload| payload.from_menu && payload.id.slot == slot.id);
let (tile, add) = ui_kit::navigation_tile(ui, slot.id, app.tr(slot.label), slot.current_icon(app.ui.visual_style), dragging);
let tile = tile.on_hover_text(&caption);
scenario::publish_target(&format!("prefs:navbar:tile:{}", slot.id), tile.rect);
scenario::publish_target(&format!("prefs:navbar:add:{}", slot.id), add.rect);
if tile.drag_started() {
@@ -359,51 +462,102 @@ fn available(app: &mut SolveApp, ui: &mut egui::Ui, state: &mut State) {
});
}
fn appearance(app: &mut SolveApp, ui: &mut egui::Ui) {
let bar = &mut app.preferences.navigation_bar;
ui_kit::settings_form(ui, Tokens::get().form_label_width, |ui| {
segments(
ui,
"position",
&mut match bar.position {
Position::Left => 0,
Position::Centre => 1,
Position::Right => 2,
},
&["navbar.custom.position_left", "navbar.custom.position_centre", "navbar.custom.position_right"],
|i| {
bar.position = match i {
0 => Position::Left,
2 => Position::Right,
_ => Position::Centre,
}
},
);
segments(ui, "edge", &mut usize::from(bar.edge == Edge::Top), &["navbar.custom.edge_bottom", "navbar.custom.edge_top"], |i| {
bar.edge = if i == 1 { Edge::Top } else { Edge::Bottom }
fn option_grid(bar: &mut NavigationBar, ui: &mut egui::Ui, first: usize) {
let t = Tokens::get();
let columns = if ui.available_width() >= t.space_24 * 24.0 {
3
} else if ui.available_width() >= t.space_24 * 16.0 {
2
} else {
1
};
let width = (ui.available_width() - t.space_16 * (columns - 1) as f32) / columns as f32;
for row in (first..4).step_by(columns) {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = t.space_16;
for index in row..(row + columns).min(4) {
ui.allocate_ui_with_layout(vec2(width.max(0.0), t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| match index {
0 => anchor_option(bar, ui),
1 => {
option(ui, "size", &mut usize::from(bar.size == Size::Regular), &["navbar.custom.compact", "navbar.custom.regular"], |i| {
bar.size = if i == 1 { Size::Regular } else { Size::Compact }
});
}
2 => {
option(
ui,
"buttons",
&mut usize::from(bar.labels == Labels::Labels),
&["navbar.custom.icons", "navbar.custom.labels"],
|i| bar.labels = if i == 1 { Labels::Labels } else { Labels::Icons },
);
}
3 => {
option(
ui,
"idle",
&mut match bar.idle {
Idle::Always => 0,
Idle::Fade => 1,
Idle::Hide => 2,
},
&["navbar.custom.always", "navbar.custom.fade", "navbar.custom.hide_idle"],
|i| {
bar.idle = match i {
0 => Idle::Always,
2 => Idle::Hide,
_ => Idle::Fade,
}
},
);
}
_ => {}
});
}
});
segments(ui, "size", &mut usize::from(bar.size == Size::Regular), &["navbar.custom.compact", "navbar.custom.regular"], |i| {
bar.size = if i == 1 { Size::Regular } else { Size::Compact }
});
segments(ui, "buttons", &mut usize::from(bar.labels == Labels::Labels), &["navbar.custom.icons", "navbar.custom.labels"], |i| {
bar.labels = if i == 1 { Labels::Labels } else { Labels::Icons }
});
segments(
ui,
"idle",
&mut match bar.idle {
Idle::Always => 0,
Idle::Fade => 1,
Idle::Hide => 2,
},
&["navbar.custom.always", "navbar.custom.fade", "navbar.custom.hide_idle"],
|i| {
bar.idle = match i {
0 => Idle::Always,
2 => Idle::Hide,
_ => Idle::Fade,
}
},
);
});
}
}
fn anchor_option(bar: &mut NavigationBar, ui: &mut egui::Ui) {
let selected = usize::from(bar.edge == Edge::Bottom) * 3
+ match bar.position {
Position::Left => 0,
Position::Centre => 1,
Position::Right => 2,
};
let names = [
"navbar.custom.anchor.top_left",
"navbar.custom.anchor.top_centre",
"navbar.custom.anchor.top_right",
"navbar.custom.anchor.bottom_left",
"navbar.custom.anchor.bottom_centre",
"navbar.custom.anchor.bottom_right",
]
.map(crate::i18n::tr);
if let Some(index) = ui_kit::settings_anchor_field(ui, crate::i18n::tr("navbar.custom.position"), selected, &names) {
bar.edge = if index >= 3 { Edge::Bottom } else { Edge::Top };
bar.position = match index % 3 {
0 => Position::Left,
2 => Position::Right,
_ => Position::Centre,
};
}
}
fn appearance(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
let bar = &mut app.preferences.navigation_bar;
let anchor_size = ui_kit::navigation_anchor_size();
// The anchor picker is tall: beside the others when there is room, else the first of them.
if ui.available_width() >= t.space_24 * 22.0 {
ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = t.space_16;
ui.allocate_ui_with_layout(vec2(anchor_size.x, anchor_size.y + t.space_24), egui::Layout::top_down(egui::Align::Min), |ui| {
anchor_option(bar, ui)
});
ui.vertical(|ui| option_grid(bar, ui, 1));
});
} else {
option_grid(bar, ui, 0);
}
}
+50 -13
View File
@@ -839,12 +839,12 @@ mod tests {
h.app.home.open = false;
h.call("ui.resize", serde_json::json!({"width":width,"height":1100}));
h.call("ui.prefs", serde_json::json!({"language":"en","open":"Appearance"}));
for (query, label, native) in [
("3D view colors", "3D view colors", None),
("Interface scale", "Interface scale", Some((Role::Slider, "Interface scale", "prefs:scale"))),
("Follow a theme file", "Follow a theme file", None),
("Background", "Background", Some((Role::Button, "Color", "dialog:Background:Color"))),
("Font", "Font", Some((Role::ComboBox, "Interface font", "prefs:ui_font"))),
for (query, label, row_target, native) in [
("3D view colors", "3D view colors", "dialog:row:3D view colors", None),
("Interface scale", "Interface scale", "dialog:row:Interface scale", Some((Role::Slider, "Interface scale", "prefs:scale"))),
("Follow a theme file", "Follow a theme file", "prefs:theme_follow_toggle", None),
("Background", "Background", "dialog:row:Background", Some((Role::Button, "Color", "dialog:Background:Color"))),
("Font", "Font", "dialog:row:Font", Some((Role::ComboBox, "Interface font", "prefs:ui_font"))),
] {
h.app.prefs_window.filter = query.into();
h.frames(5);
@@ -852,9 +852,11 @@ mod tests {
let matching: Vec<_> = hits.iter().filter(|hit| hit.label == label).collect();
assert_eq!(matching.len(), 1, "width={width}: {query} needs exactly one real navigation hit");
assert_eq!(matching[0].page, crate::prefs::PAGE_APPEARANCE);
let at = h.widget(label).unwrap();
// The search field also contains the query as its accessible value. Use the
// indexed row so this check measures the result rather than the search field.
let row = crate::scenario::rect_of(row_target).unwrap();
let body = crate::scenario::rect_of("prefs:page_body").unwrap();
assert!(body.contains(egui::pos2(at[0] as f32, at[1] as f32)));
assert!(body.contains(row.center()), "width={width}: {query} row {row:?} outside {body:?}");
if let Some((role, name, target)) = native {
let tree = h.output.accesskit_update.as_ref().unwrap();
let controls: Vec<_> =
@@ -900,6 +902,41 @@ mod tests {
/// A multi-word right-to-left label reaches the search in display order while the query is
/// typed in reading order: typing the label's words as written must find the row (#10471).
#[test]
fn layout_controls_keep_one_real_search_target_and_native_names() {
use egui::accesskit::Role;
for width in [600, 1600] {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.call("ui.resize", serde_json::json!({"width":width,"height":1100}));
h.call("ui.prefs", serde_json::json!({"language":"en","open":"Layout"}));
for (query, native) in [
("History", None),
("Browser", None),
("Components start open", Some((Role::CheckBox, "prefs:browser_components_open"))),
("Menus", None),
("Highlight active commands", Some((Role::CheckBox, "prefs:persistent_ribbon_pressed"))),
("Show navigation bar", Some((Role::CheckBox, "prefs:navbar:show"))),
("Position", None),
] {
h.app.prefs_window.filter = query.into();
h.frames(5);
let (_, _, hits) = nav(&h.ctx);
let matching: Vec<_> = hits.iter().filter(|hit| hit.page == crate::prefs::PAGE_LAYOUT && hit.label == query).collect();
assert_eq!(matching.len(), 1, "{width}: {query} has one real Layout target");
if let Some((role, target)) = native {
let tree = h.output.accesskit_update.as_ref().unwrap();
let nodes: Vec<_> =
tree.nodes.iter().filter(|(_, node)| node.role() == role && node.label() == Some(query) && !node.is_disabled()).collect();
assert_eq!(nodes.len(), 1, "{query} retains its native accessible name");
let bounds = nodes[0].1.bounds().unwrap();
let centre = egui::pos2(((bounds.x0 + bounds.x1) / 2.0) as f32, ((bounds.y0 + bounds.y1) / 2.0) as f32);
assert!(crate::scenario::rect_of(target).unwrap().contains(centre));
}
}
}
}
#[test]
fn search_finds_right_to_left_labels_typed_in_reading_order() {
let _language = crate::i18n::scope("he");
@@ -1287,10 +1324,10 @@ mod tests {
("status bar", "Status bar"),
("sketch tab shown", "Keep the Sketch tab shown"),
("search results", "Show search results in the ribbon"),
("play history", "Play History button in the History controls"),
("view's corner", "OK and Cancel in the view's corner"),
("highlight active", "Highlight active ribbon commands"),
("reveal on hover", "Reveal on hover"),
("play history", "Play History button"),
("view's corner", "Also in the view's corner"),
("highlight active", "Highlight active commands"),
("On hover", "On hover"),
];
for (query, label) in rows {
h.app.prefs_window.filter = query.into();
@@ -1304,7 +1341,7 @@ mod tests {
h.app.prefs_window.filter = "sketch tab shown".into();
h.frames(4);
assert!(drawn(&h, "Keep the Sketch tab shown"));
assert!(!drawn(&h, "Highlight active ribbon commands"), "its neighbour is not a match");
assert!(!drawn(&h, "Highlight active commands"), "its neighbour is not a match");
assert!(!drawn(&h, "Show search results in the ribbon"), "its neighbour is not a match");
h.app.prefs_window.filter = "zzzz-no-setting".into();
h.frames(4);
+2 -2
View File
@@ -248,9 +248,9 @@ fn actual_config_appearance_preferences_and_menu_boxes_and_ticks_match_at_1x_2x(
appearance.local.overwrite = true;
app.ui.pick_bodies = true;
if name == "preferences" {
// Layout still contains the real shared checkboxes, including a checked default.
// Find the redesigned Layout checkbox by its current visible label.
crate::prefs::open_at(&mut app, crate::prefs::PAGE_LAYOUT);
app.prefs_window.filter = app.tr("prefs.persistent_ribbon_pressed").to_string();
app.prefs_window.filter = app.tr("prefs.layout_page.active_commands").to_string();
}
let mut textures = HashMap::new();
let mut final_output = None;
+26 -1
View File
@@ -207,7 +207,12 @@ pub(crate) fn segmented(ui: &mut Ui, selected: &mut usize, options: &[&str], con
}
}
if t.borders.bevel.is_none() {
ui.painter().rect_stroke(rect, t.control_radius, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
ui.painter().rect_stroke(
rect,
t.control_radius,
t.control_border_stroke(if options.len() == 1 && states.first() == Some(&true) { t.form_primary } else { t.control_border }),
egui::StrokeKind::Inside,
);
}
scenario::publish_target_as(&format!("dialog:{}", label(ui)), &format!("dialog:segments:{}", label(ui)), rect);
clicked
@@ -478,6 +483,26 @@ pub(crate) fn choice_chip(ui: &mut Ui, key: &str, text: &str, selected: bool) ->
response
}
/// An independently indexed toolbar visibility chip using the standard icon segments.
pub(crate) fn settings_tab_chip(ui: &mut Ui, key: &str, text: &str, icon: &str, on: bool) -> bool {
let t = Tokens::get();
let width = (super::ContentWidth::name(ui, text) + t.space_16 + t.space_14 + t.space_6 + t.space_4).min(dimension(ui.available_width()));
ui.allocate_ui_with_layout(vec2(width, t.control_height), egui::Layout::top_down(egui::Align::Min), |ui| {
super::settings_form(ui, 0.0, |ui| {
super::row(ui, text, |ui| {
let target = format!("prefs:toolbar_tab:{key}");
segmented(
ui,
&mut 0,
&[text],
Segments { states: &[on], icons: &[icon], targets: &[&target], compact: true, intrinsic: true, ..Default::default() },
)
})
})
})
.inner
}
/// Thumbnail variant of the shared selection chip. No document or render-job ownership.
#[derive(Default)]
pub(crate) struct ThumbnailChoice {
+7 -1
View File
@@ -520,7 +520,7 @@ const ELEMENTS: &[Element] = &[
|ui| {
text(ui, "Controls", Text::Inline);
},
|ui, ()| layout_preview(ui, LayoutPreview { compact: true, labels: false, history: 2, translucent: false }),
|ui, ()| layout_preview(ui, LayoutPreview { compact: true, labels: false, history: 2, translucent: false, dialog_left: true }),
)
});
}),
@@ -950,6 +950,12 @@ const ELEMENTS: &[Element] = &[
ui.label("Paused after the configured step limit.");
});
}),
element!("settings_tab_chip", |ui, _| {
ui.horizontal_wrapped(|ui| {
settings_tab_chip(ui, "MODEL", "Model", crate::workspace::tab_icon("MODEL"), true);
settings_tab_chip(ui, "SURFACE", "Surface", crate::workspace::tab_icon("SURFACE"), false);
});
}),
element!("history_transport", |ui, _| {
ui.horizontal(|ui| {
let side = Tokens::get().space_24;
+38
View File
@@ -254,6 +254,44 @@ fn carousel_arrows_expose_disabled_state_hover_and_keyboard_focus() {
assert!(scenario::rect_of("keyboard:item:2").is_some());
}
#[test]
fn layout_picture_choices_wrap_inside_the_indexed_row_and_keep_real_targets() {
let previous = crate::theme::is_dark();
for dark in [false, true] {
crate::theme::set_dark(dark);
for width in [140.0, 280.0, 646.0] {
let ctx = crate::fonts::test_context();
let options = [
PictureChoice { key: "bottom", label: "Bottom", figure: paint_history_bottom },
PictureChoice { key: "left", label: "Left", figure: paint_history_left },
PictureChoice { key: "under_browser", label: "Under browser", figure: paint_history_under_browser },
PictureChoice { key: "browser", label: "In browser", figure: paint_history_in_browser },
];
frame(&ctx, vec![], |ui| {
ui.set_width(width);
settings_picture_choices(ui, "geometry_history", "History", &options, 2, true);
});
let row = scenario::rect_of("dialog:row:History").unwrap();
for option in &options {
let rect = scenario::rect_of(&format!("prefs:geometry_history:{}", option.key)).unwrap();
assert!(row.expand(0.5).contains_rect(rect), "{width}: {rect:?} outside {row:?}");
assert!(rect.contains(scenario::handle_at(&format!("dialog:History:{}", option.label)).unwrap()));
}
let at = scenario::handle_at("prefs:geometry_history:left").unwrap();
for down in [true, false] {
frame(&ctx, pointer(at, down), |ui| {
ui.set_width(width);
let picked = settings_picture_choices(ui, "geometry_history", "History", &options, 2, true);
if !down {
assert_eq!(picked, Some(1), "the real card selects its option");
}
});
}
}
}
crate::theme::set_dark(previous);
}
#[test]
fn row_height_and_label_column_are_fixed_for_long_labels_and_expressions() {
let ctx = crate::fonts::test_context();
@@ -3,7 +3,9 @@ use super::{Container, Surface, navigation::*};
use crate::{icons, theme::Tokens};
use egui::{FontId, Rect, Response, Sense, Ui, pos2, vec2};
pub(crate) const NAV_STAGE_HEIGHT: f32 = 58.0;
pub(crate) const NAV_STAGE_HEIGHT: f32 = 96.0;
pub(crate) const NAV_STAGE_GRID: f32 = 16.0;
const NAV_ANCHOR_INSET: f32 = 9.0;
pub(crate) const NAV_STAGE_INSET: f32 = 12.0;
pub(crate) const NAV_STAGE_RADIUS: u8 = 8;
pub(crate) const NAV_TILE_HEIGHT: f32 = 30.0 + 2.0 * NAV_BORDER;
@@ -16,17 +18,184 @@ const NAV_TILE_INSET: f32 = 9.0;
const NAV_TILE_GAP: f32 = 7.0;
const NAV_TILE_RADIUS: u8 = 7;
/// The preview stage is a miniature of the 3D view: its background gradient and ground-grid colours.
pub(crate) fn navigation_stage(ui: &mut Ui, width: f32) -> Rect {
let t = Tokens::get();
let view = Tokens::view();
let (rect, _) = ui.allocate_exact_size(vec2(width.max(0.0), NAV_STAGE_HEIGHT), Sense::hover());
let frame = Container::new(ui.ctx(), Surface::Canvas)
.frame(ui.ctx(), t.nav_viewport, crate::frame::Side::Left)
.stroke(egui::Stroke::new(NAV_BORDER, t.dialog_border))
.corner_radius(NAV_STAGE_RADIUS);
Container::paint_frame(ui.painter(), frame, rect.shrink(NAV_BORDER));
let inner = rect.shrink(NAV_BORDER);
let outline = super::preferences::rounded_perimeter(inner, f32::from(NAV_STAGE_RADIUS));
paint_view_backdrop(ui.painter(), inner, &outline, &view);
paint_view_grid(ui.painter(), inner, f32::from(NAV_STAGE_RADIUS), &view);
ui.painter().add(egui::Shape::closed_line(outline, egui::Stroke::new(NAV_BORDER, t.dialog_border)));
rect
}
/// Fills the rounded outline with the view's vertical gradient (a fan from the centre).
fn paint_view_backdrop(painter: &egui::Painter, rect: Rect, outline: &[egui::Pos2], view: &Tokens) {
if outline.len() < 3 || rect.height() <= 0.0 {
return;
}
let mut mesh = egui::Mesh::default();
mesh.colored_vertex(rect.center(), view.viewport_top.lerp_to_gamma(view.viewport_bottom, 0.5));
for point in outline {
let along = ((point.y - rect.top()) / rect.height()).clamp(0.0, 1.0);
mesh.colored_vertex(*point, view.viewport_top.lerp_to_gamma(view.viewport_bottom, along));
}
let count = outline.len() as u32;
for index in 0..count {
mesh.add_triangle(0, 1 + index, 1 + (index + 1) % count);
}
painter.add(egui::Shape::mesh(mesh));
}
/// How far a grid line must stop short of the top or bottom for the rounded corner.
fn corner_inset(distance_from_side: f32, radius: f32) -> f32 {
if radius <= 0.0 || distance_from_side >= radius {
return 0.0;
}
let across = (radius - distance_from_side.max(0.0)).min(radius);
radius - (radius * radius - across * across).max(0.0).sqrt()
}
/// The viewport's grid, flat: minor lines every [`NAV_STAGE_GRID`], a major line every fourth.
fn paint_view_grid(painter: &egui::Painter, rect: Rect, radius: f32, view: &Tokens) {
let centre = rect.center();
let lines = |origin: f32, low: f32, high: f32| -> Vec<(f32, bool)> {
let first = ((low - origin) / NAV_STAGE_GRID).ceil() as i32;
let last = ((high - origin) / NAV_STAGE_GRID).floor() as i32;
(first..=last).map(|k| (origin + k as f32 * NAV_STAGE_GRID, k % 4 == 0)).collect()
};
let stroke = |major: bool| egui::Stroke::new(if major { 1.0 } else { 0.7 }, if major { view.grid_major } else { view.grid });
for (x, major) in lines(centre.x, rect.left() + 1.0, rect.right() - 1.0) {
let inset = corner_inset((x - rect.left()).min(rect.right() - x), radius);
painter.vline(x, (rect.top() + inset)..=(rect.bottom() - inset), stroke(major));
}
for (y, major) in lines(centre.y, rect.top() + 1.0, rect.bottom() - 1.0) {
let inset = corner_inset((y - rect.top()).min(rect.bottom() - y), radius);
painter.hline((rect.left() + inset)..=(rect.right() - inset), y, stroke(major));
}
}
/// The anchor field's miniature, measured from the shared spacing tokens.
pub(crate) fn navigation_anchor_size() -> egui::Vec2 {
let t = Tokens::get();
vec2(t.space_24 * 5.0 + t.space_4, t.space_24 * 3.0)
}
/// Six anchors (left, centre, right on the top and bottom edge) on a miniature of the view.
/// Returns the clicked anchor, row-major: 0-2 along the top, 3-5 along the bottom.
pub(crate) fn navigation_anchor_picker(ui: &mut Ui, id: &str, selected: usize, labels: &[&str; 6]) -> Option<usize> {
let t = Tokens::get();
let view = Tokens::view();
let (rect, _) = ui.allocate_exact_size(navigation_anchor_size(), Sense::hover());
let outline = super::preferences::rounded_perimeter(rect.shrink(NAV_BORDER), f32::from(t.control_radius));
paint_view_backdrop(ui.painter(), rect.shrink(NAV_BORDER), &outline, &view);
ui.painter().add(egui::Shape::closed_line(outline, egui::Stroke::new(NAV_BORDER, t.control_border)));
let pill_size = vec2(t.space_24 + t.space_6, t.space_10);
let mut clicked = None;
for (index, label) in labels.iter().enumerate() {
let (row, column) = (index / 3, index % 3);
let x = match column {
0 => rect.left() + NAV_ANCHOR_INSET + pill_size.x / 2.0,
1 => rect.center().x,
_ => rect.right() - NAV_ANCHOR_INSET - pill_size.x / 2.0,
};
let y = if row == 0 { rect.top() + NAV_ANCHOR_INSET + pill_size.y / 2.0 } else { rect.bottom() - NAV_ANCHOR_INSET - pill_size.y / 2.0 };
let pill = Rect::from_center_size(pos2(x, y), pill_size);
// The hit area is the whole third of the miniature, so the target is easy to reach.
let cell = Rect::from_min_max(
pos2(rect.left() + rect.width() * column as f32 / 3.0, rect.top() + rect.height() * row as f32 / 2.0),
pos2(rect.left() + rect.width() * (column + 1) as f32 / 3.0, rect.top() + rect.height() * (row + 1) as f32 / 2.0),
);
let response = ui.interact(cell, ui.id().with((id, "anchor", index)), Sense::click());
let on = index == selected;
let (fill, stroke) = if on {
(t.form_primary, egui::Stroke::new(NAV_BORDER, t.form_primary))
} else if response.hovered() {
(t.nav_hover, egui::Stroke::new(NAV_BORDER, t.form_primary))
} else {
(t.nav_field, egui::Stroke::new(NAV_BORDER, t.control_border))
};
ui.painter().rect(pill, 4, fill, stroke, egui::StrokeKind::Inside);
if response.has_focus() {
ui.painter().rect_stroke(
pill.expand(NAV_FOCUS_WIDTH + 1.0),
5,
egui::Stroke::new(NAV_FOCUS_WIDTH, t.form_primary),
egui::StrokeKind::Outside,
);
}
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, ui.is_enabled(), on, *label));
crate::scenario::publish_target(&format!("prefs:navbar:anchor:{index}"), cell);
if response.on_hover_text(*label).clicked() {
clicked = Some(index);
}
}
clicked
}
/// A labelled row holding the anchor picker, shaped like the other Navigation bar options.
pub(crate) fn settings_anchor_field(ui: &mut Ui, label: &str, selected: usize, names: &[&str; 6]) -> Option<usize> {
let t = Tokens::get();
super::settings_form(ui, 0.0, |ui| {
super::layout::sized_row(ui, label, navigation_anchor_size().y + t.space_16 + t.space_4, |ui| {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = t.space_4;
super::settings_inline_note(ui, label);
navigation_anchor_picker(ui, "anchor", selected, names)
})
.inner
})
})
}
/// The source of separators: a dashed, label-only chip, apart from the tiles of functional buttons.
pub(crate) fn navigation_separator_source(ui: &mut Ui, id: &str, label: &str, dragging: bool) -> (Response, Response) {
let t = Tokens::get();
let text = ui.painter().layout_no_wrap(label.to_owned(), FontId::proportional(NAV_TEXT), t.form_dim);
let width = (2.0 * NAV_BORDER + NAV_TILE_INSET + NAV_TILE_ICON + NAV_TILE_GAP + text.size().x + NAV_TILE_GAP + NAV_TILE_ADD + t.space_4)
.min(ui.max_rect().width().max(0.0));
let (rect, _) = ui.allocate_exact_size(vec2(width, NAV_TILE_HEIGHT), Sense::hover());
let response = ui.interact(rect, ui.id().with((id, "tile")), Sense::click_and_drag());
let add_rect =
Rect::from_center_size(pos2(rect.right() - NAV_BORDER - t.space_4 - NAV_TILE_ADD / 2.0, rect.center().y), vec2(NAV_TILE_ADD, NAV_TILE_ADD));
let add = ui.interact(add_rect, ui.id().with((id, "add")), Sense::click());
let mut painter = ui.painter().clone();
if dragging {
painter.multiply_opacity(NAV_TILE_DRAG_OPACITY);
}
if response.hovered() {
painter.rect_filled(rect, NAV_TILE_RADIUS, t.nav_hover);
}
let mut outline = super::preferences::rounded_perimeter(rect.shrink(NAV_BORDER / 2.0), f32::from(NAV_TILE_RADIUS));
if let Some(first) = outline.first().copied() {
outline.push(first);
}
painter.extend(egui::Shape::dashed_line(&outline, egui::Stroke::new(NAV_BORDER, t.control_border), t.space_4, t.space_3));
// The glyph is the bar's own divider, flanked by the buttons it separates.
let glyph = Rect::from_center_size(
pos2(rect.left() + NAV_BORDER + NAV_TILE_INSET + NAV_TILE_ICON / 2.0, rect.center().y),
vec2(NAV_TILE_ICON, NAV_TILE_ICON),
);
for dx in [-5.0, 5.0] {
painter.rect_filled(Rect::from_center_size(pos2(glyph.center().x + dx, glyph.center().y), vec2(t.space_3, t.space_3)), 1, t.form_dim);
}
painter.vline(glyph.center().x, glyph.y_range(), egui::Stroke::new(NAV_BORDER * 1.5, t.form_dim));
painter
.with_clip_rect(rect.intersect(ui.clip_rect()).intersect(Rect::from_min_max(rect.min, pos2(add_rect.left() - NAV_TILE_GAP, rect.bottom()))))
.galley(pos2(glyph.right() + NAV_TILE_GAP, rect.center().y - text.size().y / 2.0), text, t.form_dim);
if add.hovered() {
painter.rect_filled(add_rect, t.control_radius.saturating_sub(NAV_BORDER as u8), t.nav_hover);
}
icons::paint_ink(&painter, Rect::from_center_size(add_rect.center(), vec2(t.space_14, t.space_14)), "plus", t.form_primary);
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), label));
add.widget_info(|| {
egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::format("navbar.custom.add_slot", &[("slot", label)]))
});
(response, add)
}
pub(crate) fn navigation_tile(ui: &mut Ui, id: &str, label: &str, icon: &str, dragging: bool) -> (Response, Response) {
let t = Tokens::get();
let text = ui.painter().layout_no_wrap(label.to_owned(), FontId::proportional(NAV_TEXT), t.form_ink);
+6 -1
View File
@@ -757,15 +757,20 @@ pub(super) fn rounded_perimeter(r: Rect, radius: f32) -> Vec<Pos2> {
.collect()
}
pub(crate) use super::preferences_figure_data::{
paint_browser_card, paint_browser_column, paint_history_bottom, paint_history_in_browser, paint_history_left, paint_history_under_browser,
};
#[derive(Clone, Copy)]
pub(crate) struct LayoutPreview {
pub compact: bool,
pub labels: bool,
pub history: usize,
pub translucent: bool,
pub dialog_left: bool,
}
/// One live shell specimen follows all four layout choices; it is a schematic, not a second scene.
/// One live shell specimen follows workspace, toolbar, panel and dialog placement choices.
pub(crate) fn layout_preview(ui: &mut Ui, state: LayoutPreview) {
super::preferences_figures::paint_layout(ui, state);
}
@@ -187,7 +187,8 @@ pub(crate) fn paint_contact_shading(p: &Painter, rect: Rect, on: bool) {
/// tool marks, tree rows and one simple solid providing recognisable CAD context.
pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutPreview) {
let t = Tokens::get();
let size = vec2(super::metrics::dimension(ui.available_width()), t.space_24 * 7.0 + t.space_6);
let width = super::metrics::dimension(ui.available_width());
let size = vec2(width, width * 148.0 / 226.0);
let (outer, _) = ui.allocate_exact_size(size, egui::Sense::hover());
let p = ui.painter().with_clip_rect(outer);
t.container_rect(&p, outer, t.radius_lg, t.control_fill, t.dialog_rule);
@@ -214,12 +215,20 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
p.rect(rect, if state.compact { 0 } else { t.radius_sm }, fill, t.control_border_stroke(t.control_border), egui::StrokeKind::Inside);
if !label.is_empty() {
let centre = if h > 20.0 { c.at(x + w * 0.5, y + 7.0) } else { rect.center() };
p.text(centre, egui::Align2::CENTER_CENTER, crate::i18n::tr_label(label), egui::FontId::proportional(t.font_small), t.form_dim);
p.with_clip_rect(rect.intersect(p.clip_rect())).text(
centre,
egui::Align2::CENTER_CENTER,
crate::i18n::tr_label(label),
egui::FontId::proportional(t.font_small),
t.form_dim,
);
}
rect
};
panel(inset, inset, 120.0 - inset * 2.0, ribbon_height, "", t.toolbar);
panel(bx, top, bw, browser_height, "Browser", if state.translucent && !state.compact { t.panel.gamma_multiply(0.60) } else { t.panel });
let browser =
panel(bx, top, bw, browser_height, "Browser", if state.translucent && !state.compact { t.panel.gamma_multiply(0.60) } else { t.panel });
crate::scenario::publish_rect("prefs:layout_preview:browser", browser);
// A full row of quiet tool symbols, with label marks only when labels are enabled.
let quiet = t.form_dim.lerp_to_gamma(t.viewport_top, 0.55);
for index in 0..10 {
@@ -249,6 +258,9 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
3 => None,
_ => Some(panel(inset, bottom - 10.0, 120.0 - inset * 2.0, 10.0, "History", t.selected_bg)),
};
if let Some(history) = history {
crate::scenario::publish_rect("prefs:layout_preview:history", history);
}
if let Some(history) = history.filter(|_| state.history == 1) {
// A single vertical mark reads as the narrow icon-only history rail.
p.line_segment(
@@ -256,8 +268,18 @@ pub(crate) fn paint_layout(ui: &mut egui::Ui, state: super::preferences::LayoutP
Stroke::new(2.0, t.form_primary),
);
}
// An open command dialog demonstrates its placement without another UI host.
// The left-column dialog stays below the Browser caption and above any History sharing
// that column. Its bounds belong to the Browser region, never to the full column height.
let (dialog_x, dialog_top, dialog_height) = if state.dialog_left {
(bx, top + 12.0, (browser_height - 12.0 - gap).max(0.0))
} else {
(120.0 - inset - bw, top, (viewport_bottom - top) * 0.65)
};
let dialog = panel(dialog_x, dialog_top, bw, dialog_height, "Dialogs", t.dialog_footer);
crate::scenario::publish_rect("prefs:layout_preview:dialog", dialog);
crate::scenario::publish_rect("prefs:layout_preview", outer);
super::text(ui, crate::i18n::tr("prefs.preview_choices"), super::Text::Note("preview"));
ui.interact(outer, ui.make_persistent_id("layout_preview_help"), egui::Sense::hover()).on_hover_text(crate::i18n::tr("prefs.layout_note"));
}
#[allow(unused_imports, reason = "painted by the Preferences pages of #9875 to #9883")]
@@ -284,6 +306,43 @@ pub(crate) fn paint_theme_system(p: &Painter, rect: Rect, light: &ThemeSample, d
mod tests {
use super::orientation_axes;
#[test]
fn live_layout_preview_follows_each_choice_in_both_themes() {
use crate::ui_kit::LayoutPreview;
let previous = crate::theme::is_dark();
let baseline = LayoutPreview { compact: false, labels: true, history: 0, translucent: false, dialog_left: false };
for dark in [false, true] {
crate::theme::set_dark(dark);
let draw = |state| {
let ctx = crate::fonts::test_context();
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
ui.set_width(262.0);
super::paint_layout(ui, state);
});
format!("{:?}", output.shapes)
};
let original = draw(baseline);
for state in [
LayoutPreview { compact: true, ..baseline },
LayoutPreview { labels: false, ..baseline },
LayoutPreview { history: 1, ..baseline },
LayoutPreview { history: 2, ..baseline },
LayoutPreview { history: 3, ..baseline },
LayoutPreview { translucent: true, ..baseline },
LayoutPreview { dialog_left: true, ..baseline },
] {
assert_ne!(original, draw(state), "each actual choice changes the schematic");
}
let _ = draw(LayoutPreview { compact: false, labels: false, history: 2, translucent: true, dialog_left: true });
let browser = crate::scenario::rect_of("prefs:layout_preview:browser").unwrap();
let history = crate::scenario::rect_of("prefs:layout_preview:history").unwrap();
let dialog = crate::scenario::rect_of("prefs:layout_preview:dialog").unwrap();
assert!(browser.contains_rect(dialog), "the left-column dialog remains inside the Browser region");
assert!(!dialog.intersects(history), "the dialog cannot cover the Under browser History caption");
}
crate::theme::set_dark(previous);
}
/// Both orientation figures draw right-handed axes (#5680): seen from above, the turn from X
/// to Y and from Y to Z is counter-clockwise on screen (negative cross product, as the canvas y
/// axis points down), the way the view's axis gizmo draws them. (Z to X is not tested: with Z
@@ -4,7 +4,7 @@
{"call": "engine.execute", "params": {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}}},
{"call": "engine.execute", "params": {"command": "sketch.finish", "params": {}}},
{"call": "ui.prefs", "params": {"open": "Layout"}},
{"widget": "Top", "near": "Dialog buttons"},
{"widget": "Top", "near": "OK and Cancel"},
{"widget": "OK"},
{"expect": {"prefs": {"dialog": null, "dialog_buttons": "top"}}},
{"note": "Extrude picks the sketch's one profile; its buttons are in the title row."},
@@ -4,7 +4,7 @@
{"expect": {"prefs": {"dialog": "General"}, "same_width": "prefs:window", "right_edge": {"inner": "prefs:ok", "outer": "prefs:actions", "tol": 1}, "spans": {"inner": "prefs:footer", "outer": "prefs:window", "axis": "x", "tol": 12}}},
{"click": {"handle": "prefs:page:Appearance"}},
{"expect": {"prefs": {"dialog": "Appearance"}, "same_width": "prefs:window", "right_edge": {"inner": "prefs:ok", "outer": "prefs:actions", "tol": 1}}},
{"widget": "Layout"},
{"click": {"handle": "prefs:page:Layout"}},
{"expect": {"prefs": {"dialog": "Layout"}, "same_width": "prefs:window"}},
{"widget": "Tools"},
{"expect": {"prefs": {"dialog": "Tools"}, "same_width": "prefs:window"}},
+2 -1
View File
@@ -60,7 +60,7 @@ The interaction states (drag onto the bar, drop targets, reorder) stay as in `Na
| Label | Control | Edits | Default | Hover | Figure | Notes |
|---|---|---|---|---|---|---|
| Preview as | Tabs: Design / Render | which workspace's bar is edited | Design | none | none | The Render tab edits the Render workspace's slots. |
| Preview as | Tabs: Design / Render | previews the shared bar as Design or Render | Design | none | none | Preview modes use the Design or Render capabilities; edits affect the one shared bar in both workspaces. |
| Preview strip | The bar drawn as it will look, with the slots in order | `navigation_bar.slots` (plus `slot_options`) | orbit, pan, zoom, separator, fit, look_at, home, separator, visual_style, display | none | none | Slot icons on the board are stand-ins; use the app's icons. |
| Show navigation bar | Checkbox | `app.ui.show_navigation_bar` (session; in the Snapshot) | per session | none | none | |
| Reset to default | Button | resets `navigation_bar` | n/a | none | none | |
@@ -74,6 +74,7 @@ The interaction states (drag onto the bar, drop targets, reorder) stay as in `Na
## Mismatches
* The board implies per-workspace navigation bars; the app stores one shared slot list. Design/Render only changes its preview.
* "Compact" on the board is `LayoutMode::Docked` (serialised `compact`) in the code.
* The History choice is three booleans in the code, one card group on the board.
* The status chips ("Analysis", "Section", "Live details", "Fold items") are not on this page; they
+2 -1
View File
@@ -383,7 +383,8 @@ slot/tile/add/appearance targets retain their identities.
| Bar 9 | Tooltip delay 800ms, gap 6, padding (8,5), radius 6; 12 medium text, 11 mono key, 15 key line height | `NAV_TOOLTIP_DELAY/POPUP_GAP`, `navigation_tooltip`; `nav_tip/tip_ink/tip_key_border` |
| Bar 10 | Render hides Look at and Visual style; flat canvases hide Orbit, Spin, Look at, Projection and Visual style, retaining Home and Display | `Slot::available`, `canvas_caps` |
| Bar 11 | Context width 232; bar has Customise / Hide / Reset, slot has Remove | `NAV_CONTEXT_WIDTH`, `navigation_bar_view::context_menu` |
| Settings 1, 8 | Stage height 58, radius 8, padding 12 inside its 1 border; position follows preference live | `NAV_STAGE_HEIGHT/RADIUS/INSET`, `editor_layout` |
| Settings 1, 8 | Stage height 96, radius 8, padding 12 inside its 1 border; a miniature of the 3D view (the view's gradient and grid colours); the bar follows the chosen anchor (side and top or bottom edge) live, and the slot count sits beside it on the side facing the view's middle | `NAV_STAGE_HEIGHT/RADIUS/INSET`, `navigation_stage`, `editor_layout` |
| Settings 5 | Position is one picker of six anchors on a miniature of the view (`prefs:navbar:anchor:0..5`, top row then bottom row); the separator source is a dashed chip at the end of the Available heading, apart from the button tiles | `navigation_anchor_picker`, `navigation_separator_source` |
| Settings 1–3 | Standard settings section and full-width detail card; actions stay on one line, with name truncation before icon-only actions | `settings_section`, `settings_card`, `inspector_header_layout` |
| Settings 4, 5 | Marker 2×24 radius 1; ghost 70%, dragged tile 55%, hidden slot 40% and dashed border | `insertion_marker_with_ink`, `NAV_MARKER_HEIGHT/GHOST_OPACITY/TILE_DRAG_OPACITY/HIDDEN_OPACITY` |
| Settings 6 | Enabled Add buttons answer with “Remove a slot to add another.” at the 16-slot cap | Pure list operations in `navigation_bar.rs`; `warning` |