From 1f00b48456da1fc6c9b35f168fa08e105bfb1437 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Mon, 21 Feb 2022 15:08:33 +0100
Subject: [PATCH] [ivette] move theme CSS do dome

---
 ivette/src/dome/main/dome.ts                  | 27 ++++--
 ivette/src/{colors => dome/renderer}/dark.css |  0
 ivette/src/dome/renderer/dome.tsx             |  2 +
 ivette/src/dome/renderer/frame/toolbars.tsx   | 23 +++--
 ivette/src/dome/renderer/layout/forms.tsx     | 63 +++++++++++++
 .../src/{colors => dome/renderer}/light.css   |  0
 .../renderer/text}/dark-code.css              |  0
 ivette/src/dome/renderer/text/editors.tsx     | 32 ++++---
 ivette/src/dome/renderer/themes.tsx           | 92 ++++++++++++++++++
 ivette/src/frama-c/kernel/ASTinfo.tsx         |  3 -
 ivette/src/frama-c/kernel/ASTview.tsx         |  6 +-
 ivette/src/frama-c/kernel/SourceCode.tsx      |  6 +-
 ivette/src/ivette/prefs.tsx                   | 93 ++++++++++---------
 ivette/src/renderer/Application.tsx           | 18 +---
 ivette/src/renderer/Controller.tsx            |  4 -
 ivette/src/renderer/Preferences.tsx           | 52 +++++++----
 ivette/src/renderer/index.js                  |  2 -
 17 files changed, 298 insertions(+), 125 deletions(-)
 rename ivette/src/{colors => dome/renderer}/dark.css (100%)
 rename ivette/src/{colors => dome/renderer}/light.css (100%)
 rename ivette/src/{colors => dome/renderer/text}/dark-code.css (100%)
 create mode 100644 ivette/src/dome/renderer/themes.tsx

diff --git a/ivette/src/dome/main/dome.ts b/ivette/src/dome/main/dome.ts
index 5930229d41c..37078a13cfa 100644
--- a/ivette/src/dome/main/dome.ts
+++ b/ivette/src/dome/main/dome.ts
@@ -620,13 +620,28 @@ ipcMain.handle(
 );
 
 // --------------------------------------------------------------------------
-
-type themes = 'dark' | 'light' | 'system';
-
-ipcMain.handle('theme-color:switch', (_evt, theme: themes) => {
-  nativeTheme.themeSource = theme;
+// --- Native Theme
+// --------------------------------------------------------------------------
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+ipcMain.handle('dome.ipc.theme.setSource', (_evt, theme) => {
+  switch (theme) {
+    case 'dark':
+    case 'light':
+    case 'system':
+      nativeTheme.themeSource = theme;
+      return;
+    default:
+      console.warn('[dome] unknown theme', theme);
+  }
 });
 
-ipcMain.handle('theme-color:which-system', () => {
+ipcMain.handle('dome.ipc.theme.getDefault', () => {
   return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
 });
+
+nativeTheme.on('updated', () => {
+  broadcast('dome.theme.updated');
+});
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/colors/dark.css b/ivette/src/dome/renderer/dark.css
similarity index 100%
rename from ivette/src/colors/dark.css
rename to ivette/src/dome/renderer/dark.css
diff --git a/ivette/src/dome/renderer/dome.tsx b/ivette/src/dome/renderer/dome.tsx
index 4442d4d2ca7..1770a29b7f2 100644
--- a/ivette/src/dome/renderer/dome.tsx
+++ b/ivette/src/dome/renderer/dome.tsx
@@ -49,6 +49,8 @@ import { ipcRenderer } from 'electron';
 import SYS, * as System from 'dome/system';
 import * as Json from 'dome/data/json';
 import * as Settings from 'dome/data/settings';
+import './dark.css';
+import './light.css';
 import './style.css';
 import { State } from './data/states';
 
diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx
index 6f54b94f976..15746e533ea 100644
--- a/ivette/src/dome/renderer/frame/toolbars.tsx
+++ b/ivette/src/dome/renderer/frame/toolbars.tsx
@@ -160,23 +160,28 @@ export interface SwitchProps {
   enabled?: boolean;
   /** Defaults to `false`. */
   disabled?: boolean;
-  /** Switch position. Default to left. */
-  position?: 'left' | 'right';
+  /** Switch value.
+      When checked, the slide is switched to « right » position.
+      Defaults to false. */
+  checked?: boolean;
   /** Click callback. */
-  onChange?: (newPosition: 'left' | 'right') => void;
+  onChange?: (newValue: boolean) => void;
+  /** Right Click callback. */
+  onContextMenu?: () => void;
 }
 
 /** Toolbar Switch. */
 export function Switch(props: SwitchProps) {
-  const { position = 'left', onChange } = props;
+  const { checked = false, onChange } = props;
   const { title = '', className = '', style } = props;
   const { enabled = true, disabled = false } = props;
-  const slide = (p: 'left' | 'right') => p === 'left' ? 'right' : 'left';
-  const callback = onChange && (() => onChange(slide(position)));
-  const checked = position === 'right';
-  if (disabled || !enabled) return <></>;
+  const callback = onChange && (() => onChange(!checked));
+  if (disabled || !enabled) return null;
   return (
-    <label className={'dome-xSwitch ' + className} style={style} >
+    <label
+      className={'dome-xSwitch ' + className}
+      style={style}
+      onContextMenu={props.onContextMenu}>
       <input type={'checkbox'} checked={checked} onChange={callback} />
       <span className={'dome-xSwitch-slider'} title={title} />
     </label>
diff --git a/ivette/src/dome/renderer/layout/forms.tsx b/ivette/src/dome/renderer/layout/forms.tsx
index d18cea9ae1d..638517f281b 100644
--- a/ivette/src/dome/renderer/layout/forms.tsx
+++ b/ivette/src/dome/renderer/layout/forms.tsx
@@ -1266,4 +1266,67 @@ export function SelectField(props: SelectFieldProps) {
   );
 }
 
+/** @category Form Fields */
+export interface MenuFieldOption<A> {
+  value: A;
+  label: string;
+}
+
+/** @category Form Fields */
+export interface MenuFieldProps<A> extends FieldProps<A> {
+  /** Field label. */
+  label: string;
+  /** Field tooltip text. */
+  title?: string;
+  /** Field state. */
+  state: FieldState<A>;
+  placeholder?: string;
+  defaultValue: A;
+  options: MenuFieldOption<A>[];
+}
+
+type ENTRY<A> = { option: JSX.Element, field: string, value: A };
+
+/**
+   Creates a `<SelectField/>` form field with a predefine set
+   of (typed) options.
+
+   @category Form Fields
+ */
+export function MenuField<A>(props: MenuFieldProps<A>): JSX.Element {
+  const entries: ENTRY<A>[] = React.useMemo(() =>
+    props.options.map((e, k) => {
+      const field = `item#${k}`;
+      const option = <option value={field} label={e.label} />;
+      return { field, option, value: e.value };
+    }), [props.options]);
+  const input = React.useCallback(
+    (v) => entries.find((e) => e.value === v)?.field
+    , [entries]
+  );
+  const output = React.useCallback(
+    (f) => entries.find((e) => e.field === f)?.value ?? props.defaultValue
+    , [entries, props.defaultValue]
+  );
+  const defaultField = React.useMemo(
+    () => input(props.defaultValue),
+    [input, props.defaultValue]
+  );
+  const state = useFilter<A, string | undefined>(
+    props.state,
+    input, output,
+    defaultField,
+  );
+  return (
+    <SelectField
+      state={state}
+      label={props.label}
+      title={props.title}
+      placeholder={props.placeholder} >
+      {entries.map((e) => e.option)}
+    </SelectField>
+  );
+}
+
+
 // --------------------------------------------------------------------------
diff --git a/ivette/src/colors/light.css b/ivette/src/dome/renderer/light.css
similarity index 100%
rename from ivette/src/colors/light.css
rename to ivette/src/dome/renderer/light.css
diff --git a/ivette/src/colors/dark-code.css b/ivette/src/dome/renderer/text/dark-code.css
similarity index 100%
rename from ivette/src/colors/dark-code.css
rename to ivette/src/dome/renderer/text/dark-code.css
diff --git a/ivette/src/dome/renderer/text/editors.tsx b/ivette/src/dome/renderer/text/editors.tsx
index b3cf82b06d3..43df5396e92 100644
--- a/ivette/src/dome/renderer/text/editors.tsx
+++ b/ivette/src/dome/renderer/text/editors.tsx
@@ -34,11 +34,13 @@
 import _ from 'lodash';
 import React from 'react';
 import * as Dome from 'dome';
+import * as Themes from 'dome/themes';
 import { Vfill } from 'dome/layout/boxes';
 import CodeMirror, { EditorConfiguration } from 'codemirror/lib/codemirror';
 import { RichTextBuffer, CSSMarker, Decorator } from './buffers';
 
 import './style.css';
+import './dark-code.css';
 import 'codemirror/lib/codemirror.css';
 
 const CSS_HOVERED = 'dome-xText-hover';
@@ -487,8 +489,7 @@ class CodeMirrorWrapper extends React.Component<TextProps> {
 // --- Text View
 // --------------------------------------------------------------------------
 
-/**
-   #### Text Editor.
+/** #### Text Editor.
 
    A component rendering the content of a text buffer, that shall be instances
    of the `Buffer` base class.
@@ -506,11 +507,17 @@ class CodeMirrorWrapper extends React.Component<TextProps> {
 
    #### Themes
 
-   The CodeMirror `theme` option allow you to style your document,
-   especially when using modes.
-   Themes are only accessible if you load the associated CSS style sheet.
-   For instance, to use the `'ambiance'` theme provided with CodeMirror, you
-   shall import `'codemirror/theme/ambiance.css'` somewhere in your application.
+   The CodeMirror `theme` option allow you to style your document, especially
+   when using modes.
+
+   By default, CodeMirror uses the `'default'` theme in _light_ theme and the
+   `'dark-code'` theme in _dark_ theme. The `'dark-code'` is provided by Dome,
+   Cf. `./dark-mode.css` in the source distribution.
+
+   To use other custom themes, you shall load the associated CSS style
+   sheet. For instance, to use the `'ambiance'` theme provided with CodeMirror,
+   you shall import `'codemirror/theme/ambiance.css'` somewhere in your
+   application.
 
    #### Modes & Adds-On
 
@@ -524,16 +531,17 @@ class CodeMirrorWrapper extends React.Component<TextProps> {
    You can register your own extensions directly into the global `CodeMirror`
    class instance.  However, the correct instance must be retrieved by using
    `import CodeMirror from 'codemirror/lib/codemirror.js'` ; using `from
-   'codemirror'` returns a different instance of `CodeMirror` class and will
-   not work.
- */
+   'codemirror'` returns a different instance of `CodeMirror` class and will not
+   work.  */
 export function Text(props: TextProps) {
-  let { className, style, fontSize, ...cmprops } = props;
+  const [appTheme] = Themes.useColorTheme();
+  let { className, style, fontSize, theme: usrTheme, ...cmprops } = props;
   if (fontSize !== undefined && fontSize < 4) fontSize = 4;
   if (fontSize !== undefined && fontSize > 48) fontSize = 48;
+  const theme = usrTheme ?? (appTheme === 'dark' ? 'dark-code' : 'default');
   return (
     <Vfill className={className} style={{ ...style, fontSize }}>
-      <CodeMirrorWrapper fontSize={fontSize} {...cmprops} />
+      <CodeMirrorWrapper fontSize={fontSize} theme={theme} {...cmprops} />
     </Vfill>
   );
 }
diff --git a/ivette/src/dome/renderer/themes.tsx b/ivette/src/dome/renderer/themes.tsx
new file mode 100644
index 00000000000..8bb3e2a3bef
--- /dev/null
+++ b/ivette/src/dome/renderer/themes.tsx
@@ -0,0 +1,92 @@
+/* ************************************************************************ */
+/*                                                                          */
+/*   This file is part of Frama-C.                                          */
+/*                                                                          */
+/*   Copyright (C) 2007-2021                                                */
+/*     CEA (Commissariat à l'énergie atomique et aux énergies               */
+/*          alternatives)                                                   */
+/*                                                                          */
+/*   you can redistribute it and/or modify it under the terms of the GNU    */
+/*   Lesser General Public License as published by the Free Software        */
+/*   Foundation, version 2.1.                                               */
+/*                                                                          */
+/*   It is distributed in the hope that it will be useful,                  */
+/*   but WITHOUT ANY WARRANTY; without even the implied warranty of         */
+/*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          */
+/*   GNU Lesser General Public License for more details.                    */
+/*                                                                          */
+/*   See the GNU Lesser General Public License version 2.1                  */
+/*   for more details (enclosed in the file licenses/LGPLv2.1).             */
+/*                                                                          */
+/* ************************************************************************ */
+
+// --------------------------------------------------------------------------
+// --- Global Color Theme Management
+// --------------------------------------------------------------------------
+
+/**
+   @packageDocumentation
+   @module dome/themes
+ */
+
+//import React from 'react';
+import * as Dome from 'dome';
+import * as Settings from 'dome/data/settings';
+import { State } from 'dome/data/states';
+import { ipcRenderer } from 'electron';
+
+/* -------------------------------------------------------------------------- */
+/* --- Global Settings                                                    --- */
+/* -------------------------------------------------------------------------- */
+
+export type ColorTheme = 'dark' | 'light';
+export type ColorSettings = 'dark' | 'light' | 'system';
+
+export const jColorTheme =
+  (th: string | undefined): ColorTheme => (th === 'dark' ? 'dark' : 'light');
+export const jColorSettings =
+  (th: string | undefined): ColorSettings => {
+    switch (th) {
+      case 'light':
+      case 'dark':
+      case 'system':
+        return th;
+      default:
+        return 'system';
+    }
+  };
+
+const ColorThemeSettings = new Settings.GString('dome-color-theme', 'system');
+const NativeThemeUpdated = new Dome.Event('dome.theme.updated');
+ipcRenderer.on('dome.theme.updated', () => NativeThemeUpdated.emit());
+
+async function getNativeTheme(): Promise<ColorTheme> {
+  const th = await ipcRenderer.invoke('dome.ipc.theme.getDefault');
+  return jColorTheme(th);
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- Color Theme Hooks                                                  --- */
+/* -------------------------------------------------------------------------- */
+
+export function useColorTheme(): [ColorTheme, (upd: ColorSettings) => void] {
+  Dome.useUpdate(NativeThemeUpdated);
+  const { result: current } = Dome.usePromise(getNativeTheme());
+  const [pref, setPref] = Settings.useGlobalSettings(ColorThemeSettings);
+  const setTheme = (upd: ColorSettings): void => {
+    setPref(upd);
+    ipcRenderer.invoke('dome.ipc.theme.setSource', upd);
+  };
+  return [current ?? jColorTheme(pref), setTheme];
+}
+
+export function useColorThemeSettings(): State<ColorSettings> {
+  const [pref, setPref] = Settings.useGlobalSettings(ColorThemeSettings);
+  const setTheme = (upd: ColorSettings): void => {
+    setPref(upd);
+    ipcRenderer.invoke('dome.ipc.theme.setSource', upd);
+  };
+  return [jColorSettings(pref), setTheme];
+}
+
+/* -------------------------------------------------------------------------- */
diff --git a/ivette/src/frama-c/kernel/ASTinfo.tsx b/ivette/src/frama-c/kernel/ASTinfo.tsx
index 9ae52f7ec89..928411368bd 100644
--- a/ivette/src/frama-c/kernel/ASTinfo.tsx
+++ b/ivette/src/frama-c/kernel/ASTinfo.tsx
@@ -27,7 +27,6 @@
 import React from 'react';
 import * as States from 'frama-c/states';
 import * as Utils from 'frama-c/utils';
-import * as Preferences from 'ivette/prefs';
 
 import { Vfill } from 'dome/layout/boxes';
 import { RichTextBuffer } from 'dome/text/buffers';
@@ -40,7 +39,6 @@ import { getInfo } from 'frama-c/kernel/api/ast';
 
 export default function ASTinfo(): JSX.Element {
 
-  const theme = Preferences.useThemeColors();
   const buffer = React.useMemo(() => new RichTextBuffer(), []);
   const [selection, updateSelection] = States.useSelection();
   const marker = selection?.current?.marker;
@@ -66,7 +64,6 @@ export default function ASTinfo(): JSX.Element {
         <Text
           buffer={buffer}
           mode="text"
-          theme={theme}
           onSelection={onTextSelection}
           readOnly
         />
diff --git a/ivette/src/frama-c/kernel/ASTview.tsx b/ivette/src/frama-c/kernel/ASTview.tsx
index cebfda57c71..d3505aba569 100644
--- a/ivette/src/frama-c/kernel/ASTview.tsx
+++ b/ivette/src/frama-c/kernel/ASTview.tsx
@@ -164,9 +164,8 @@ export default function ASTview() {
   const multipleSelections = selection?.multiple.allSelections;
   const theFunction = selection?.current?.fct;
   const theMarker = selection?.current?.marker;
-  const { buttons: themeButtons, theme, fontSize, wrapText } =
-    Preferences.useThemeButtons({
-      target: 'Internal AST',
+  const { buttons: themeButtons, fontSize, wrapText } =
+    Preferences.useEditorButtons({
       fontSize: Preferences.AstFontSize,
       wrapText: Preferences.AstWrapText,
       disabled: !theFunction,
@@ -310,7 +309,6 @@ export default function ASTview() {
       <Text
         buffer={buffer}
         mode="text/x-csrc"
-        theme={theme}
         fontSize={fontSize}
         lineWrapping={wrapText}
         selection={theMarker}
diff --git a/ivette/src/frama-c/kernel/SourceCode.tsx b/ivette/src/frama-c/kernel/SourceCode.tsx
index 882765aa55c..4edabcf138e 100644
--- a/ivette/src/frama-c/kernel/SourceCode.tsx
+++ b/ivette/src/frama-c/kernel/SourceCode.tsx
@@ -80,9 +80,8 @@ export default function SourceCode(): JSX.Element {
   const filename = Path.parse(file).base;
 
   // Title bar buttons, along with the parameters for our text.
-  const { buttons: themeButtons, theme, fontSize, wrapText } =
-    Preferences.useThemeButtons({
-      target: 'Source Code',
+  const { buttons: themeButtons, fontSize, wrapText } =
+    Preferences.useEditorButtons({
       fontSize: Preferences.SourceFontSize,
       wrapText: Preferences.AstWrapText,
       disabled: !theFunction,
@@ -202,7 +201,6 @@ export default function SourceCode(): JSX.Element {
       <Text
         buffer={buffer}
         mode="text/x-csrc"
-        theme={theme}
         fontSize={fontSize}
         lineWrapping={wrapText}
         selection={theMarker}
diff --git a/ivette/src/ivette/prefs.tsx b/ivette/src/ivette/prefs.tsx
index 039b544df6d..ba20eeced76 100644
--- a/ivette/src/ivette/prefs.tsx
+++ b/ivette/src/ivette/prefs.tsx
@@ -20,8 +20,6 @@
 /*                                                                          */
 /* ************************************************************************ */
 
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
-
 // --------------------------------------------------------------------------
 // --- Main React Component rendered by './index.js'
 // --------------------------------------------------------------------------
@@ -32,76 +30,76 @@
  */
 
 import React from 'react';
-
-import { usePromise } from 'dome/dome';
+import * as Dome from 'dome';
+import * as Themes from 'dome/themes';
+import * as Toolbar from 'dome/frame/toolbars';
 import * as Settings from 'dome/data/settings';
 import { IconButton } from 'dome/controls/buttons';
-
 import 'codemirror/mode/clike/clike';
-import '../colors/dark-code.css';
-import { ipcRenderer } from 'electron';
-
-export const THEMES = [
-  { id: 'light', label: 'Light' },
-  { id: 'dark', label: 'Dark' },
-  { id: 'system', label: 'System Defaults' },
-];
 
 // --------------------------------------------------------------------------
 // --- AST View Preferences
 // --------------------------------------------------------------------------
 
-export const ColorTheme = new Settings.GString('color-theme', 'system');
 export const AstFontSize = new Settings.GNumber('ASTview.fontSize', 12);
 export const AstWrapText = new Settings.GFalse('ASTview.wrapText');
 export const SourceFontSize = new Settings.GNumber('SourceCode.fontSize', 12);
 export const SourceWrapText = new Settings.GFalse('SourceCode.wrapText');
 
-export interface ThemeProps {
-  target: string;
-  fontSize: Settings.GlobalSettings<number>;
-  wrapText: Settings.GlobalSettings<boolean>;
-  disabled?: boolean;
+/* -------------------------------------------------------------------------- */
+/* --- Theme Switcher Button                                              --- */
+/* -------------------------------------------------------------------------- */
+
+const themeEntries: Dome.PopupMenuItem[] = [
+  { id: 'light', label: 'Switch to Light Theme' },
+  { id: 'dark', label: 'Switch to Dark Theme' },
+  { id: 'system', label: 'Switch to System Default' },
+];
+
+export function ThemeSwitch(): JSX.Element {
+  const [theme, setTheme] = Themes.useColorTheme();
+  const other = theme === 'dark' ? 'light' : 'dark';
+  const title = `Switch to ${other} theme (right-click for full choice)`;
+  const onChange = (): void => setTheme(other);
+  const onPopup = (): void => Dome.popupMenu(
+    themeEntries,
+    (th) => setTheme(Themes.jColorSettings(th))
+  );
+  return (
+    <Toolbar.Switch
+      disabled={!Dome.DEVEL}
+      title={title}
+      checked={theme === 'dark'}
+      onChange={onChange}
+      onContextMenu={onPopup}
+    />
+  );
 }
 
 // --------------------------------------------------------------------------
-// --- Icon Buttons
+// --- Editor Icon Buttons
 // --------------------------------------------------------------------------
 
-export interface ThemeControls {
+export interface EditorProps {
+  fontSize: Settings.GlobalSettings<number>;
+  wrapText: Settings.GlobalSettings<boolean>;
+  disabled?: boolean;
+}
+
+export interface EditorControls {
   buttons: React.ReactNode;
-  theme: string;
   fontSize: number;
   wrapText: boolean;
 }
 
-export function forceThemeUpdate(theme: string) {
-  ipcRenderer.invoke('theme-color:switch', theme);
-}
-
-ipcRenderer.on('dome.ipc.settings.defaults', () => {
-  forceThemeUpdate('system');
-});
-
-export function useThemeColors() {
-  const [themeColors] = Settings.useGlobalSettings(ColorTheme);
-  const invoke = () => ipcRenderer.invoke('theme-color:which-system');
-  const { result }: { result: 'dark' | 'light' } = usePromise(invoke());
-  if (themeColors === 'system')
-    return result === 'dark' ? 'dark-code' : 'default';
-  return themeColors === 'dark' ? 'dark-code' : 'default';
-}
-
-export function useThemeButtons(props: ThemeProps): ThemeControls {
+export function useEditorButtons(props: EditorProps): EditorControls {
+  const { disabled = false } = props;
   const [fontSize, setFontSize] = Settings.useGlobalSettings(props.fontSize);
   const [wrapText, setWrapText] = Settings.useGlobalSettings(props.wrapText);
-  const theme = useThemeColors();
-  const zoomIn = () => fontSize < 48 && setFontSize(fontSize + 2);
-  const zoomOut = () => fontSize > 4 && setFontSize(fontSize - 2);
-  const flipWrapText = () => setWrapText(!wrapText);
-  const { disabled = false } = props;
+  const zoomIn = (): void => setFontSize(fontSize + 2);
+  const zoomOut = (): void => setFontSize(fontSize - 2);
+  const flipWrapText = (): void => setWrapText(!wrapText);
   return {
-    theme,
     fontSize,
     wrapText,
     buttons: [
@@ -109,6 +107,7 @@ export function useThemeButtons(props: ThemeProps): ThemeControls {
         key="zoom.out"
         icon="ZOOM.OUT"
         onClick={zoomOut}
+        enabled={fontSize > 4}
         disabled={disabled}
         title="Decrease font size"
       />,
@@ -116,6 +115,7 @@ export function useThemeButtons(props: ThemeProps): ThemeControls {
         key="zoom.in"
         icon="ZOOM.IN"
         onClick={zoomIn}
+        enabled={fontSize < 48}
         disabled={disabled}
         title="Increase font size"
       />,
@@ -123,6 +123,7 @@ export function useThemeButtons(props: ThemeProps): ThemeControls {
         key="wrap"
         icon="WRAPTEXT"
         selected={wrapText}
+        disabled={disabled}
         onClick={flipWrapText}
         title="Wrap text"
       />,
diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx
index df336b19ed3..ea4a07a1a0b 100644
--- a/ivette/src/renderer/Application.tsx
+++ b/ivette/src/renderer/Application.tsx
@@ -35,9 +35,8 @@ import * as Sidebar from 'dome/frame/sidebars';
 import * as Controller from './Controller';
 import * as Extensions from './Extensions';
 import * as Laboratory from './Laboratory';
-import * as Settings from 'dome/data/settings';
+import * as IvettePrefs from 'ivette/prefs';
 import './loader';
-import * as Preferences from 'ivette/prefs';
 
 // --------------------------------------------------------------------------
 // --- Main View
@@ -52,14 +51,6 @@ export default function Application(): JSX.Element {
   const onSelectedHints = (): void => {
     if (hints.length === 1) Extensions.onSearchHint(hints[0]);
   };
-
-  const [ th, setTh ] = Settings.useGlobalSettings(Preferences.ColorTheme);
-  const change = Preferences.forceThemeUpdate;
-  React.useState(() => change(th));
-  const other = th === 'dark' ? 'light' : 'dark';
-  const themeTitle = 'Switch to ' + other + ' theme';
-  const changeColorTheme: () => void = () => { change(other); setTh(other); };
-
   return (
     <Vfill>
       <Toolbar.ToolBar>
@@ -79,12 +70,7 @@ export default function Application(): JSX.Element {
           onHint={Extensions.onSearchHint}
           onSelect={onSelectedHints}
         />
-        <Toolbar.Switch
-          disabled={!Dome.DEVEL}
-          title={themeTitle}
-          position={th === 'dark' ? 'right' : 'left'}
-          onChange={changeColorTheme}
-        />
+        <IvettePrefs.ThemeSwitch />
         <Toolbar.Button
           icon="ITEMS.GRID"
           title="Customize Main View"
diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx
index df50ecbcbc3..5d4d89e6564 100644
--- a/ivette/src/renderer/Controller.tsx
+++ b/ivette/src/renderer/Controller.tsx
@@ -37,8 +37,6 @@ import { LED, LEDstatus } from 'dome/controls/displays';
 import { Label, Code } from 'dome/controls/labels';
 import { RichTextBuffer } from 'dome/text/buffers';
 import { Text } from 'dome/text/editors';
-import * as Preferences from 'ivette/prefs';
-
 import * as Ivette from 'ivette';
 import * as Server from 'frama-c/server';
 
@@ -197,7 +195,6 @@ export const Control = () => {
 const editor = new RichTextBuffer();
 
 const RenderConsole = () => {
-  const theme = Preferences.useThemeColors();
   const scratch = React.useRef([] as string[]);
   const [cursor, setCursor] = React.useState(-1);
   const [isEmpty, setEmpty] = React.useState(true);
@@ -332,7 +329,6 @@ const RenderConsole = () => {
         buffer={edited ? editor : Server.buffer}
         mode="text"
         readOnly={!edited}
-        theme={theme}
       />
     </>
   );
diff --git a/ivette/src/renderer/Preferences.tsx b/ivette/src/renderer/Preferences.tsx
index b9f5959eaf6..8056ec84f5f 100644
--- a/ivette/src/renderer/Preferences.tsx
+++ b/ivette/src/renderer/Preferences.tsx
@@ -39,13 +39,41 @@ import React from 'react';
 
 import * as Settings from 'dome/data/settings';
 import * as Forms from 'dome/layout/forms';
+import * as Themes from 'dome/themes';
 import * as IvettePrefs from 'ivette/prefs';
 
 // --------------------------------------------------------------------------
-// --- Font Forms
+// --- Theme Fields
 // --------------------------------------------------------------------------
 
-function ThemeFields(props: IvettePrefs.ThemeProps) {
+const themeOptions: Forms.MenuFieldOption<Themes.ColorSettings>[] = [
+  { value: 'light', label: 'Light Theme' },
+  { value: 'dark', label: 'Dark Theme' },
+  { value: 'system', label: 'System Defaults' },
+];
+
+function ThemeFields(): JSX.Element {
+  const state = Forms.useValid(Themes.useColorThemeSettings());
+  return (
+    <Forms.MenuField<Themes.ColorSettings>
+      label="Color Theme"
+      title="Select global color theme for the application"
+      state={state}
+      defaultValue='system'
+      options={themeOptions}
+    />
+  );
+}
+
+// --------------------------------------------------------------------------
+// --- Editor Fields
+// --------------------------------------------------------------------------
+
+interface EditorFieldProps extends IvettePrefs.EditorProps {
+  target: string;
+}
+
+function EditorFields(props: EditorFieldProps) {
   const fontsize = Forms.useValid(
     Settings.useGlobalSettings(props.fontSize),
   );
@@ -72,21 +100,7 @@ function ThemeFields(props: IvettePrefs.ThemeProps) {
   );
 }
 
-function ColorThemeFields() {
-  const [theme, setTheme] = Settings.useGlobalSettings(IvettePrefs.ColorTheme);
-  const elements = IvettePrefs.THEMES.map(({ id, label }) => {
-    return <option value={id} key={id}>{label}</option>;
-  });
-  const set = (t: string | undefined) => {
-    if (t) { IvettePrefs.forceThemeUpdate(t); setTheme(t); }
-  };
-  return (
-    <Forms.SelectField label={'Color theme'} state={[theme, undefined, set]}>
-      {elements}
-    </Forms.SelectField>
-  );
 
-}
 
 // --------------------------------------------------------------------------
 // --- Editor Command Forms
@@ -110,17 +124,17 @@ export default function Preferences() {
   return (
     <Forms.Page>
       <Forms.Section label="Theme" unfold>
-        <ColorThemeFields />
+        <ThemeFields />
       </Forms.Section>
       <Forms.Section label="AST View" unfold>
-        <ThemeFields
+        <EditorFields
           target="Internal AST"
           fontSize={IvettePrefs.AstFontSize}
           wrapText={IvettePrefs.AstWrapText}
         />
       </Forms.Section>
       <Forms.Section label="Source View" unfold>
-        <ThemeFields
+        <EditorFields
           target="Source Code"
           fontSize={IvettePrefs.SourceFontSize}
           wrapText={IvettePrefs.SourceWrapText}
diff --git a/ivette/src/renderer/index.js b/ivette/src/renderer/index.js
index d65ee906500..b7fa5b20332 100644
--- a/ivette/src/renderer/index.js
+++ b/ivette/src/renderer/index.js
@@ -46,8 +46,6 @@ import {
   isApplicationWindow,
   isPreferencesWindow,
 } from 'dome' ;
-import '../colors/light.css';
-import '../colors/dark.css';
 
 // You can change the name of the main components,
 // provided you define the makefile variable
-- 
GitLab