├── .github
└── workflows
│ └── azure-swa.yml
├── .gitignore
├── Directory.Build.props
├── FluentEditor.sln
├── FluentEditorBrowser
├── AppBundle
│ ├── Logo.svg
│ ├── app.css
│ ├── favicon.ico
│ ├── index.html
│ ├── main.js
│ └── staticwebapp.config.json
├── FluentEditorBrowser.csproj
├── Program.cs
└── runtimeconfig.template.json
├── FluentEditorDesktop
├── FluentEditorDesktop.csproj
├── Program.cs
└── app.manifest
├── FluentEditorShared
├── App.axaml
├── App.axaml.cs
├── ColorPalette
│ ├── ColorPalette.cs
│ ├── ColorPaletteEditor.cs
│ ├── ColorPaletteEntry.cs
│ ├── ColorPaletteEntryEditor.cs
│ ├── ContrastColorWrapper.cs
│ ├── ContrastListItem.cs
│ ├── EditableColorPaletteEntry.cs
│ ├── ExpandedColorPaletteEntryEditor.axaml
│ ├── ExpandedColorPaletteEntryEditor.axaml.cs
│ ├── IColorPalette.cs
│ └── IColorPaletteEntry.cs
├── ControlPalette
│ ├── ColorMapping.cs
│ ├── ControlPaletteTestContent.axaml
│ ├── ControlPaletteTestContent.axaml.cs
│ ├── ControlPaletteView.axaml
│ ├── ControlPaletteView.axaml.cs
│ ├── ControlPaletteViewModel.cs
│ ├── ExportView.axaml
│ ├── ExportView.axaml.cs
│ ├── ExportViewModel.cs
│ └── Model
│ │ ├── ControlPaletteExportProvider.cs
│ │ ├── ControlPaletteModel.cs
│ │ └── Preset.cs
├── Converters.cs
├── FluentEditorShared.csproj
├── FluentEditorSharedResources.axaml
├── LocExtension.cs
├── Model
│ └── MainNavModel.cs
├── OuterNav
│ ├── INavItem.cs
│ ├── OuterNavPage.axaml
│ ├── OuterNavPage.axaml.cs
│ └── OuterNavViewModel.cs
├── Resources
│ ├── Resources.en-US.resx
│ ├── Resources.resx
│ ├── controlPaletteData.json
│ └── demodata.json
├── StringProvider.cs
└── Utils
│ ├── ColorBlending.cs
│ ├── ColorScale.cs
│ ├── ColorTypes.cs
│ ├── ColorUtils.cs
│ ├── JSONExtensionMethods.cs
│ └── MathUtils.cs
├── LICENSE
├── README.md
└── README_Images
├── App.png
├── ColorContrast_Bad.png
├── ColorContrast_Good.png
├── ExportTheme.png
├── RegionBasePrimary_DetailColorProperties.png
└── RegionBasePrimary_Properties.png
/.github/workflows/azure-swa.yml:
--------------------------------------------------------------------------------
1 | name: Azure Static Web Apps CI/CD
2 |
3 | on:
4 | push:
5 | branches:
6 | - release/*
7 |
8 | jobs:
9 | build_and_deploy_job:
10 | runs-on: ubuntu-latest
11 | name: Build and Deploy Job
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | submodules: recursive
16 |
17 | - name: Setup .NET 7
18 | uses: actions/setup-dotnet@v3
19 | with:
20 | dotnet-version: 7.0.100
21 |
22 | - name: Install wasm-tools
23 | run: dotnet workload install wasm-tools wasm-experimental
24 |
25 | - name: Install DotNetCompress
26 | run: dotnet tool install --global DotNetCompress --version 1.0.0-preview.7 --no-cache
27 |
28 | - name: Publish .NET Project
29 | run: dotnet publish FluentEditorBrowser/FluentEditorBrowser.csproj -c Release -o release --nologo
30 |
31 | - name: Brotli Compress Output (dll)
32 | run: DotNetCompress -d FluentEditorBrowser/bin/Release/net7.0/browser-wasm/AppBundle/ -p '*.dll' -p '*.js' -p '*.symbols' -p '*.blat' -p '*.bin' -p '*.wasm'
33 |
34 | - name: Build And Deploy
35 | id: builddeploy
36 | uses: Azure/static-web-apps-deploy@v1
37 | with:
38 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APP_DEPLOY_KEY }}
39 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
40 | action: "upload"
41 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
42 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
43 | app_location: "FluentEditorBrowser/bin/Release/net7.0/browser-wasm/AppBundle/" # App source code path
44 | api_location: "" # Api source code path - optional
45 | output_location: "" # Modify this to where your SSG places the built HTML - could be `dist`, `build`... check your config
46 | skip_app_build: true
47 | ###### End of Repository/Build Configurations ######
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | 11.0-rc1.1
5 | true
6 |
7 |
8 |
9 | False
10 | None
11 |
12 |
13 |
--------------------------------------------------------------------------------
/FluentEditor.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29324.140
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEditorShared", "FluentEditorShared\FluentEditorShared.csproj", "{42A312EF-7C4A-4661-A684-DB14E40A2A90}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEditorBrowser", "FluentEditorBrowser\FluentEditorBrowser.csproj", "{B52910F3-58C8-4253-8B1F-952A7DDBE2AE}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentEditorDesktop", "FluentEditorDesktop\FluentEditorDesktop.csproj", "{9A8C8058-E145-4644-A7A9-FA0792ACCDCE}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{73ED66C7-8A44-40E8-A1CD-024DDD55BAAD}"
13 | ProjectSection(SolutionItems) = preProject
14 | .gitignore = .gitignore
15 | Directory.Build.props = Directory.Build.props
16 | README.md = README.md
17 | EndProjectSection
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Debug|ARM = Debug|ARM
23 | Debug|x64 = Debug|x64
24 | Debug|x86 = Debug|x86
25 | Release|Any CPU = Release|Any CPU
26 | Release|ARM = Release|ARM
27 | Release|x64 = Release|x64
28 | Release|x86 = Release|x86
29 | EndGlobalSection
30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
31 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|ARM.ActiveCfg = Debug|ARM
34 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|ARM.Build.0 = Debug|ARM
35 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|x64.ActiveCfg = Debug|x64
36 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|x64.Build.0 = Debug|x64
37 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|x86.ActiveCfg = Debug|x86
38 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Debug|x86.Build.0 = Debug|x86
39 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|ARM.ActiveCfg = Release|ARM
42 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|ARM.Build.0 = Release|ARM
43 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|x64.ActiveCfg = Release|x64
44 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|x64.Build.0 = Release|x64
45 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|x86.ActiveCfg = Release|x86
46 | {42A312EF-7C4A-4661-A684-DB14E40A2A90}.Release|x86.Build.0 = Release|x86
47 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
49 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|ARM.ActiveCfg = Debug|Any CPU
50 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|ARM.Build.0 = Debug|Any CPU
51 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|x64.ActiveCfg = Debug|Any CPU
52 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|x64.Build.0 = Debug|Any CPU
53 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|x86.ActiveCfg = Debug|Any CPU
54 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Debug|x86.Build.0 = Debug|Any CPU
55 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|ARM.ActiveCfg = Release|Any CPU
58 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|ARM.Build.0 = Release|Any CPU
59 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|x64.ActiveCfg = Release|Any CPU
60 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|x64.Build.0 = Release|Any CPU
61 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|x86.ActiveCfg = Release|Any CPU
62 | {B52910F3-58C8-4253-8B1F-952A7DDBE2AE}.Release|x86.Build.0 = Release|Any CPU
63 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
65 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|ARM.ActiveCfg = Debug|Any CPU
66 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|ARM.Build.0 = Debug|Any CPU
67 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|x64.ActiveCfg = Debug|Any CPU
68 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|x64.Build.0 = Debug|Any CPU
69 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|x86.ActiveCfg = Debug|Any CPU
70 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Debug|x86.Build.0 = Debug|Any CPU
71 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
72 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|Any CPU.Build.0 = Release|Any CPU
73 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|ARM.ActiveCfg = Release|Any CPU
74 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|ARM.Build.0 = Release|Any CPU
75 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|x64.ActiveCfg = Release|Any CPU
76 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|x64.Build.0 = Release|Any CPU
77 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|x86.ActiveCfg = Release|Any CPU
78 | {9A8C8058-E145-4644-A7A9-FA0792ACCDCE}.Release|x86.Build.0 = Release|Any CPU
79 | EndGlobalSection
80 | GlobalSection(SolutionProperties) = preSolution
81 | HideSolutionNode = FALSE
82 | EndGlobalSection
83 | GlobalSection(ExtensibilityGlobals) = postSolution
84 | SolutionGuid = {7EA96CDE-0998-4389-A162-963C75A4307E}
85 | EndGlobalSection
86 | EndGlobal
87 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/Logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --sat: env(safe-area-inset-top);
3 | --sar: env(safe-area-inset-right);
4 | --sab: env(safe-area-inset-bottom);
5 | --sal: env(safe-area-inset-left);
6 | }
7 |
8 | /* HTML styles for the splash screen */
9 |
10 | .highlight {
11 | color: white;
12 | font-size: 2.5rem;
13 | display: block;
14 | }
15 |
16 | .purple {
17 | color: #8b44ac;
18 | }
19 |
20 | .icon {
21 | opacity: 0.05;
22 | height: 35%;
23 | width: 35%;
24 | position: absolute;
25 | background-repeat: no-repeat;
26 | right: 0px;
27 | bottom: 0px;
28 | margin-right: 3%;
29 | margin-bottom: 5%;
30 | z-index: 5000;
31 | background-position: right bottom;
32 | pointer-events: none;
33 | }
34 |
35 | #avalonia-splash a {
36 | color: whitesmoke;
37 | text-decoration: none;
38 | }
39 |
40 | .center {
41 | display: flex;
42 | justify-content: center;
43 | align-items: center;
44 | height: 100vh;
45 | }
46 |
47 | #avalonia-splash {
48 | position: relative;
49 | height: 100%;
50 | width: 100%;
51 | color: whitesmoke;
52 | background: #1b2a4e;
53 | font-family: 'Nunito', sans-serif;
54 | background-position: center;
55 | background-size: cover;
56 | background-repeat: no-repeat;
57 | justify-content: center;
58 | align-items: center;
59 | }
60 |
61 | .splash-close {
62 | animation: fadeout 0.25s linear forwards;
63 | }
64 |
65 | @keyframes fadeout {
66 | 0% {
67 | opacity: 100%;
68 | }
69 |
70 | 100% {
71 | opacity: 0;
72 | visibility: collapse;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/FluentEditorBrowser/AppBundle/favicon.ico
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FluentEditor
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |

25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/main.js:
--------------------------------------------------------------------------------
1 | import { dotnet } from './dotnet.js'
2 |
3 | const is_browser = typeof window != "undefined";
4 | if (!is_browser) throw new Error(`Expected to be running in a browser`);
5 |
6 | const dotnetRuntime = await dotnet
7 | .withDiagnosticTracing(false)
8 | .withApplicationArgumentsFromQuery()
9 | .create();
10 |
11 | const config = dotnetRuntime.getConfig();
12 |
13 | await dotnetRuntime.runMainAndExit(config.mainAssemblyName, [window.location.search]);
14 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/AppBundle/staticwebapp.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "route": "/*",
5 | "headers": {
6 | "Cache-Control": "no-cache"
7 | }
8 | }
9 | ]
10 | }
--------------------------------------------------------------------------------
/FluentEditorBrowser/FluentEditorBrowser.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | browser-wasm
6 | AppBundle\main.js
7 | Exe
8 | true
9 | true
10 | FluentEditor
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.Versioning;
2 | using System.Threading.Tasks;
3 | using Avalonia;
4 | using Avalonia.Browser;
5 |
6 | [assembly: System.Resources.NeutralResourcesLanguageAttribute("en")]
7 | [assembly: SupportedOSPlatform("browser")]
8 |
9 | namespace FluentEditor;
10 |
11 | internal class Program
12 | {
13 | private static Task Main(string[] args)
14 | {
15 | return BuildAvaloniaApp()
16 | .WithInterFont()
17 | .StartBrowserAppAsync("out");
18 | }
19 |
20 | public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure();
21 | }
22 |
--------------------------------------------------------------------------------
/FluentEditorBrowser/runtimeconfig.template.json:
--------------------------------------------------------------------------------
1 | {
2 | "wasmHostProperties": {
3 | "perHostConfig": [
4 | {
5 | "name": "browser",
6 | "html-path": "index.html",
7 | "Host": "browser"
8 | }
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/FluentEditorDesktop/FluentEditorDesktop.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net7.0
5 | true
6 | app.manifest
7 | 0.1.0
8 |
9 | true
10 | true
11 | FluentEditor
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/FluentEditorDesktop/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | [assembly: System.Resources.NeutralResourcesLanguageAttribute("en")]
5 |
6 | namespace FluentEditor;
7 |
8 | internal class Program
9 | {
10 | [STAThread]
11 | public static void Main(string[] args) => BuildAvaloniaApp()
12 | .StartWithClassicDesktopLifetime(args);
13 |
14 | public static AppBuilder BuildAvaloniaApp()
15 | {
16 | return AppBuilder.Configure()
17 | .UsePlatformDetect()
18 | .WithInterFont()
19 | .AfterSetup(_ =>
20 | {
21 | #if DEBUG
22 | Application.Current!.AttachDevTools();
23 | #endif
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/FluentEditorDesktop/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/FluentEditorShared/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/FluentEditorShared/App.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.Threading.Tasks;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Controls.ApplicationLifetimes;
8 | using Avalonia.Markup.Xaml;
9 | using Avalonia.Platform.Storage;
10 | using FluentEditor.ControlPalette.Model;
11 | using FluentEditor.Model;
12 | using FluentEditor.OuterNav;
13 | using FluentEditorShared;
14 |
15 | namespace FluentEditor
16 | {
17 | public sealed class App : Application
18 | {
19 | public static FilePickerFileType JsonFileType { get; } = new("JSON")
20 | {
21 | Patterns = new[] { "*.json" },
22 | MimeTypes = new[] { "application/json" },
23 | AppleUniformTypeIdentifiers = new[] { "public.json" }
24 | };
25 |
26 | public static OuterNavPage NavPage { get; private set; }
27 | public static IStringProvider StringProvider { get; private set; }
28 |
29 | public App()
30 | {
31 | AvaloniaXamlLoader.Load(this);
32 | }
33 |
34 | public override async void OnFrameworkInitializationCompleted()
35 | {
36 | base.OnFrameworkInitializationCompleted();
37 |
38 | await SetupDependencies();
39 |
40 | NavPage = new OuterNavPage(_outerNavViewModel);
41 |
42 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime classicDesktopStyleApplicationLifetime)
43 | {
44 | var window = new Window
45 | {
46 | Content = NavPage
47 | };
48 | classicDesktopStyleApplicationLifetime.MainWindow = window;
49 | classicDesktopStyleApplicationLifetime.Exit += (sender, args) =>
50 | {
51 | _ = SuspendApp(_mainNavModel, _paletteModel);
52 | };
53 | window.Show();
54 | }
55 | else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewApplicationLifetime)
56 | {
57 | singleViewApplicationLifetime.MainView = NavPage;
58 | }
59 | }
60 |
61 | private async Task SuspendApp(IMainNavModel mainModel, IControlPaletteModel controlPaletteModel)
62 | {
63 | await controlPaletteModel.HandleAppSuspend();
64 | await mainModel.HandleAppSuspend();
65 | }
66 |
67 | private async Task SetupDependencies()
68 | {
69 | var stringProvider = new StringProvider();
70 | var exportProvider = new ControlPaletteExportProvider();
71 |
72 | var paletteModel = new ControlPaletteModel();
73 | await paletteModel.InitializeData(stringProvider, stringProvider.GetString("ControlPaletteDataPath"));
74 |
75 | var navModel = new MainNavModel(stringProvider);
76 | await navModel.InitializeData(stringProvider.GetString("MainDataPath"), paletteModel, exportProvider);
77 |
78 | _stringProvider = stringProvider;
79 | _exportProvider = exportProvider;
80 |
81 | _paletteModel = paletteModel;
82 |
83 | _mainNavModel = navModel;
84 | _outerNavViewModel = new OuterNavViewModel(_mainNavModel.NavItems, _mainNavModel.DefaultNavItem);
85 | StringProvider = stringProvider;
86 | }
87 |
88 | private StringProvider _stringProvider;
89 | private ControlPaletteExportProvider _exportProvider;
90 | private IMainNavModel _mainNavModel;
91 | private IControlPaletteModel _paletteModel;
92 |
93 | private OuterNavViewModel _outerNavViewModel;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ColorPaletteEditor.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.Collections.Generic;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Controls.Primitives;
8 |
9 | namespace FluentEditorShared.ColorPalette
10 | {
11 | public class ColorPaletteEditor : TemplatedControl
12 | {
13 | #region ColorPaletteProperty
14 |
15 | public static readonly StyledProperty ColorPaletteProperty = AvaloniaProperty.Register("ColorPalette");
16 |
17 | private void OnColorPaletteChanged(ColorPalette oldValue, ColorPalette newValue)
18 | {
19 | if(newValue == null)
20 | {
21 | SetValue(PaletteEntriesProperty, null);
22 | }
23 | else
24 | {
25 | SetValue(PaletteEntriesProperty, newValue.Palette);
26 | }
27 | }
28 |
29 | public ColorPalette ColorPalette
30 | {
31 | get => GetValue(ColorPaletteProperty);
32 | set => SetValue(ColorPaletteProperty, value);
33 | }
34 |
35 | #endregion
36 |
37 | #region PaletteEntriesProperty
38 |
39 | public static readonly StyledProperty> PaletteEntriesProperty = AvaloniaProperty.Register>("PaletteEntries");
40 |
41 | public IReadOnlyList PaletteEntries => GetValue(PaletteEntriesProperty);
42 |
43 | #endregion
44 |
45 | #region IsExpandedProperty
46 |
47 | public static readonly StyledProperty IsExpandedProperty = AvaloniaProperty.Register("IsExpanded", true);
48 |
49 | public bool IsExpanded
50 | {
51 | get => GetValue(IsExpandedProperty);
52 | set => SetValue(IsExpandedProperty, value);
53 | }
54 |
55 | #endregion
56 |
57 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
58 | {
59 | base.OnPropertyChanged(change);
60 |
61 | if (change.Property == ColorPaletteProperty)
62 | {
63 | var (oldValue, newValue) = change.GetOldAndNewValue();
64 | OnColorPaletteChanged(oldValue, newValue);
65 | }
66 | else if (change.Property == IsExpandedProperty)
67 | {
68 | PseudoClasses.Set(":expanded", IsExpanded);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ColorPaletteEntry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text.Json.Nodes;
7 | using Avalonia.Media;
8 | using FluentEditorShared.Utils;
9 |
10 | namespace FluentEditorShared.ColorPalette
11 | {
12 | // These classes are not intended to be viewmodels.
13 | // They deal with the data about an editable palette and are passed to special purpose controls for editing
14 | public class ColorPaletteEntry : IColorPaletteEntry
15 | {
16 | public static ColorPaletteEntry Parse(JsonObject data, IReadOnlyList contrastColors)
17 | {
18 | Color color;
19 | if (data.ContainsKey("Color"))
20 | {
21 | color = data["Color"].GetColor();
22 | }
23 | else
24 | {
25 | color = default(Color);
26 | }
27 |
28 | ColorStringFormat activeColorStringFormat = ColorStringFormat.PoundRGB;
29 | if (data.ContainsKey("ActiveColorStringFormat"))
30 | {
31 | activeColorStringFormat = data.GetEnum();
32 | }
33 |
34 | return new ColorPaletteEntry(color, data.GetOptionalString("Title"), data.GetOptionalString("Description"), activeColorStringFormat, contrastColors);
35 | }
36 |
37 | public ColorPaletteEntry(Color color, string title, string description, ColorStringFormat activeColorStringFormat, IReadOnlyList contrastColors)
38 | {
39 | _activeColor = color;
40 | _title = title;
41 | _description = description;
42 | _activeColorStringFormat = activeColorStringFormat;
43 |
44 | ContrastColors = contrastColors;
45 | }
46 |
47 | private string _title;
48 | public string Title
49 | {
50 | get => _title;
51 | set => _title = value;
52 | }
53 |
54 | private string _description;
55 | public string Description
56 | {
57 | get => _description;
58 | set => _description = value;
59 | }
60 |
61 | private Color _activeColor;
62 | public Color ActiveColor
63 | {
64 | get => _activeColor;
65 | set
66 | {
67 | if (_activeColor != value)
68 | {
69 | _activeColor = value;
70 | ActiveColorChanged?.Invoke(this);
71 |
72 | UpdateContrastColor();
73 | }
74 | }
75 | }
76 |
77 | public string ActiveColorString => ColorUtils.FormatColorString(_activeColor, _activeColorStringFormat);
78 |
79 | private ColorStringFormat _activeColorStringFormat = ColorStringFormat.PoundRGB;
80 | public ColorStringFormat ActiveColorStringFormat => _activeColorStringFormat;
81 |
82 | public event Action ActiveColorChanged;
83 |
84 | private IReadOnlyList _contrastColors;
85 | public IReadOnlyList ContrastColors
86 | {
87 | get => _contrastColors;
88 | set
89 | {
90 | if (_contrastColors != value)
91 | {
92 | if (_contrastColors != null)
93 | {
94 | foreach (var c in _contrastColors)
95 | {
96 | c.Color.ActiveColorChanged -= ContrastColor_ActiveColorChanged;
97 | }
98 | }
99 |
100 | _contrastColors = value;
101 |
102 | if (_contrastColors != null)
103 | {
104 | foreach (var c in _contrastColors)
105 | {
106 | c.Color.ActiveColorChanged += ContrastColor_ActiveColorChanged;
107 | }
108 | }
109 |
110 | UpdateContrastColor();
111 | }
112 | }
113 | }
114 |
115 | private void ContrastColor_ActiveColorChanged(IColorPaletteEntry obj)
116 | {
117 | UpdateContrastColor();
118 | }
119 |
120 | private ContrastColorWrapper _bestContrastColor;
121 | public ContrastColorWrapper BestContrastColor => _bestContrastColor;
122 |
123 | public double BestContrastValue
124 | {
125 | get
126 | {
127 | if (_bestContrastColor == null)
128 | {
129 | return 0;
130 | }
131 | return ColorUtils.ContrastRatio(ActiveColor, _bestContrastColor.Color.ActiveColor);
132 | }
133 | }
134 |
135 | private void UpdateContrastColor()
136 | {
137 | ContrastColorWrapper newContrastColor = null;
138 |
139 | if (_contrastColors != null && _contrastColors.Count > 0)
140 | {
141 | double maxContrast = -1;
142 | foreach (var c in _contrastColors)
143 | {
144 | double contrast = ColorUtils.ContrastRatio(ActiveColor, c.Color.ActiveColor);
145 | if (contrast > maxContrast)
146 | {
147 | maxContrast = contrast;
148 | newContrastColor = c;
149 | }
150 | }
151 | }
152 |
153 | if (_bestContrastColor != newContrastColor)
154 | {
155 | _bestContrastColor = newContrastColor;
156 | ContrastColorChanged?.Invoke(this);
157 | }
158 | }
159 |
160 | public event Action ContrastColorChanged;
161 | }
162 | }
163 |
164 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ColorPaletteEntryEditor.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Avalonia;
5 | using Avalonia.Controls;
6 | using Avalonia.Interactivity;
7 | using Avalonia.Layout;
8 | using Avalonia.Markup.Xaml.Templates;
9 | using Avalonia.Media;
10 | using Avalonia.Styling;
11 |
12 | namespace FluentEditorShared.ColorPalette
13 | {
14 | public enum ColorPaletteEntryCaptionMode { None, ActiveColorString, Title }
15 |
16 | public class ColorPaletteEntryEditor : Button
17 | {
18 | public ColorPaletteEntryEditor()
19 | {
20 | // Make sure each instance of the control gets its own brush instances
21 | ActiveColorBrush = new SolidColorBrush();
22 | ContrastColorBrush = new SolidColorBrush();
23 |
24 | Click += ColorPaletteEntryEditor_Click;
25 | }
26 |
27 | #region ColorPaletteEntryProperty
28 |
29 | public static readonly StyledProperty ColorPaletteEntryProperty = AvaloniaProperty.Register("ColorPaletteEntry");
30 |
31 | private void OnColorPaletteEntryChanged(IColorPaletteEntry oldValue, IColorPaletteEntry newValue)
32 | {
33 | if (oldValue != null)
34 | {
35 | oldValue.ActiveColorChanged -= ColorPaletteEntry_ActiveColorChanged;
36 | oldValue.ContrastColorChanged -= ColorPaletteEntry_ContrastColorChanged;
37 | }
38 |
39 | if (newValue != null)
40 | {
41 | ActiveColorBrush.Color = newValue.ActiveColor;
42 | ContrastColorBrush.Color = newValue.BestContrastColor != null ? newValue.BestContrastColor.Color.ActiveColor : default(Color);
43 | newValue.ActiveColorChanged += ColorPaletteEntry_ActiveColorChanged;
44 | newValue.ContrastColorChanged += ColorPaletteEntry_ContrastColorChanged;
45 |
46 | if (_flyoutContent != null)
47 | {
48 | _flyoutContent.Content = newValue;
49 | }
50 | }
51 | else
52 | {
53 | HideFlyout();
54 | ActiveColorBrush.Color = default(Color);
55 | ContrastColorBrush.Color = default(Color);
56 | }
57 |
58 | UpdateCaption();
59 | }
60 |
61 | public IColorPaletteEntry ColorPaletteEntry
62 | {
63 | get => GetValue(ColorPaletteEntryProperty);
64 | set => SetValue(ColorPaletteEntryProperty, value);
65 | }
66 |
67 | #endregion
68 |
69 | #region CaptionModeProperty
70 |
71 | public static readonly StyledProperty CaptionModeProperty = AvaloniaProperty.Register("CaptionMode", ColorPaletteEntryCaptionMode.ActiveColorString);
72 |
73 | public ColorPaletteEntryCaptionMode CaptionMode
74 | {
75 | get => GetValue(CaptionModeProperty);
76 | set => SetValue(CaptionModeProperty, value);
77 | }
78 |
79 | #endregion
80 |
81 | #region CaptionProperty
82 |
83 | public static readonly StyledProperty CaptionProperty = AvaloniaProperty.Register("Caption");
84 |
85 | public string Caption
86 | {
87 | get => GetValue(CaptionProperty);
88 | private set => SetValue(CaptionProperty, value);
89 | }
90 |
91 | #endregion
92 |
93 | #region FlyoutTemplateProperty
94 |
95 | public static readonly StyledProperty FlyoutTemplateProperty = AvaloniaProperty.Register("FlyoutTemplate");
96 |
97 | public DataTemplate FlyoutTemplate
98 | {
99 | get => GetValue(FlyoutTemplateProperty);
100 | set => SetValue(FlyoutTemplateProperty, value);
101 | }
102 |
103 | #endregion
104 |
105 | #region FlyoutPresenterStyleProperty
106 |
107 | public static readonly StyledProperty FlyoutPresenterStyleProperty = AvaloniaProperty.Register("FlyoutPresenterStyle");
108 |
109 | public ControlTheme FlyoutPresenterStyle
110 | {
111 | get => GetValue(FlyoutPresenterStyleProperty);
112 | set => SetValue(FlyoutPresenterStyleProperty, value);
113 | }
114 |
115 | #endregion
116 |
117 | #region ActiveColorBrushProperty
118 |
119 | public static readonly StyledProperty ActiveColorBrushProperty = AvaloniaProperty.Register("ActiveColorBrush", new SolidColorBrush());
120 |
121 | public SolidColorBrush ActiveColorBrush
122 | {
123 | get => GetValue(ActiveColorBrushProperty);
124 | private set => SetValue(ActiveColorBrushProperty, value);
125 | }
126 |
127 | #endregion
128 |
129 | #region ContrastColorBrushProperty
130 |
131 | public static readonly StyledProperty ContrastColorBrushProperty = AvaloniaProperty.Register("ContrastColorBrush", new SolidColorBrush());
132 |
133 | public SolidColorBrush ContrastColorBrush
134 | {
135 | get => GetValue(ContrastColorBrushProperty);
136 | private set => SetValue(ContrastColorBrushProperty, value);
137 | }
138 |
139 | #endregion
140 |
141 | private void ColorPaletteEntry_ActiveColorChanged(IColorPaletteEntry obj)
142 | {
143 | var paletteEntry = ColorPaletteEntry;
144 | if (paletteEntry == null)
145 | {
146 | return;
147 | }
148 | ActiveColorBrush.Color = paletteEntry.ActiveColor;
149 |
150 | UpdateCaption();
151 | }
152 |
153 | private void ColorPaletteEntry_ContrastColorChanged(IColorPaletteEntry obj)
154 | {
155 | var paletteEntry = ColorPaletteEntry;
156 | if (paletteEntry == null)
157 | {
158 | return;
159 | }
160 | ContrastColorBrush.Color = paletteEntry.BestContrastColor != null ? paletteEntry.BestContrastColor.Color.ActiveColor : default(Color);
161 | }
162 |
163 | private void UpdateCaption()
164 | {
165 | var captionMode = CaptionMode;
166 | var paletteEntry = ColorPaletteEntry;
167 | switch (captionMode)
168 | {
169 | case ColorPaletteEntryCaptionMode.None:
170 | Caption = string.Empty;
171 | break;
172 | case ColorPaletteEntryCaptionMode.ActiveColorString:
173 | Caption = paletteEntry != null ? paletteEntry.ActiveColorString : string.Empty;
174 | break;
175 | case ColorPaletteEntryCaptionMode.Title:
176 | Caption = paletteEntry != null ? paletteEntry.Title : string.Empty;
177 | break;
178 | }
179 | }
180 |
181 | private void ColorPaletteEntryEditor_Click(object sender, RoutedEventArgs e)
182 | {
183 | ShowFlyout();
184 | }
185 |
186 | private Flyout _flyout;
187 | private ContentControl _flyoutContent;
188 |
189 | private void ShowFlyout()
190 | {
191 | HideFlyout();
192 |
193 | var flyoutTemplate = FlyoutTemplate;
194 | if (flyoutTemplate == null)
195 | {
196 | return;
197 | }
198 |
199 | var paletteEntry = ColorPaletteEntry;
200 |
201 | Flyout flyout = new Flyout();
202 |
203 | var flyoutPresenterStyle = FlyoutPresenterStyle;
204 | if(flyoutPresenterStyle != null)
205 | {
206 | flyout.FlyoutPresenterTheme = flyoutPresenterStyle;
207 | }
208 |
209 | ContentControl flyoutContent = new ContentControl();
210 | flyoutContent.ContentTemplate = flyoutTemplate;
211 | flyoutContent.Content = paletteEntry;
212 | flyoutContent.HorizontalAlignment = HorizontalAlignment.Stretch;
213 | flyoutContent.VerticalAlignment = VerticalAlignment.Stretch;
214 | flyoutContent.Margin = new Thickness(0);
215 | flyoutContent.Padding = new Thickness(0);
216 |
217 | flyout.Content = flyoutContent;
218 |
219 | flyout.ShowAt(this);
220 | }
221 |
222 | private void HideFlyout()
223 | {
224 | if (_flyout != null)
225 | {
226 | _flyout.Hide();
227 | _flyout = null;
228 | _flyoutContent = null;
229 | }
230 | }
231 |
232 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
233 | {
234 | base.OnPropertyChanged(change);
235 |
236 | if (change.Property == CaptionModeProperty)
237 | {
238 | UpdateCaption();
239 | }
240 | else if (change.Property == ColorPaletteEntryProperty)
241 | {
242 | var (oldValue, newValue) = change.GetOldAndNewValue();
243 | OnColorPaletteEntryChanged(oldValue, newValue);
244 | }
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ContrastColorWrapper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 |
6 | namespace FluentEditorShared.ColorPalette
7 | {
8 | public class ContrastColorWrapper
9 | {
10 | public ContrastColorWrapper(IColorPaletteEntry color, bool showInContrastList, bool showContrastErrors)
11 | {
12 | if(color == null)
13 | {
14 | throw new ArgumentNullException("color");
15 | }
16 | Color = color;
17 | ShowInContrastList = showInContrastList;
18 | ShowContrastErrors = showContrastErrors;
19 | }
20 |
21 | public IColorPaletteEntry Color { get; }
22 |
23 | public bool ShowInContrastList { get; }
24 |
25 | public bool ShowContrastErrors { get; }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ContrastListItem.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.ComponentModel;
5 | using System.Runtime.CompilerServices;
6 | using Avalonia.Media;
7 | using FluentEditorShared.Utils;
8 |
9 | namespace FluentEditorShared.ColorPalette
10 | {
11 | public class ContrastListItem : INotifyPropertyChanged
12 | {
13 | public ContrastListItem() { }
14 | public ContrastListItem(IColorPaletteEntry baseColor, IColorPaletteEntry contrastColor, double minContrast = 4.5)
15 | {
16 | _minContrast = minContrast;
17 | _baseColorBrush = new SolidColorBrush();
18 | _contrastColorBrush = new SolidColorBrush();
19 |
20 | BaseColor = baseColor;
21 | ContrastColor = contrastColor;
22 | }
23 |
24 | private void RecalcContrast()
25 | {
26 | if (_baseColor == null || _contrastColor == null)
27 | {
28 | Contrast = 0.0;
29 | return;
30 | }
31 |
32 | Contrast = ColorUtils.ContrastRatio(_baseColor.ActiveColor, _contrastColor.ActiveColor);
33 | }
34 |
35 | private double _minContrast;
36 | private double _contrast;
37 | public double Contrast
38 | {
39 | get => _contrast;
40 | private set
41 | {
42 | if (_contrast != value)
43 | {
44 | _contrast = value;
45 | RaisePropertyChangedFromSource();
46 |
47 | RaisePropertyChanged("ContrastError");
48 | }
49 | }
50 | }
51 |
52 | public bool ContrastError
53 | {
54 | get
55 | {
56 | if(_minContrast <= 0)
57 | {
58 | return false;
59 | }
60 | return _contrast < _minContrast;
61 | }
62 | }
63 |
64 | private IColorPaletteEntry _baseColor;
65 | public IColorPaletteEntry BaseColor
66 | {
67 | get => _baseColor;
68 | set
69 | {
70 | if (_baseColor != value)
71 | {
72 | if (_baseColor != null)
73 | {
74 | _baseColor.ActiveColorChanged -= _baseColor_ActiveColorChanged;
75 | }
76 |
77 | _baseColor = value;
78 | RaisePropertyChangedFromSource();
79 |
80 | if (_baseColor != null)
81 | {
82 | _baseColorBrush.Color = _baseColor.ActiveColor;
83 | _baseColor.ActiveColorChanged += _baseColor_ActiveColorChanged;
84 | }
85 |
86 | RecalcContrast();
87 | }
88 | }
89 | }
90 |
91 | private void _baseColor_ActiveColorChanged(IColorPaletteEntry obj)
92 | {
93 | _baseColorBrush.Color = obj.ActiveColor;
94 | RecalcContrast();
95 | }
96 |
97 | private SolidColorBrush _baseColorBrush;
98 | public SolidColorBrush BaseColorBrush => _baseColorBrush;
99 |
100 | private IColorPaletteEntry _contrastColor;
101 | public IColorPaletteEntry ContrastColor
102 | {
103 | get => _contrastColor;
104 | set
105 | {
106 | if (_contrastColor != value)
107 | {
108 | if (_contrastColor != null)
109 | {
110 | _contrastColor.ActiveColorChanged -= _contrastColor_ActiveColorChanged;
111 | }
112 |
113 | _contrastColor = value;
114 | RaisePropertyChangedFromSource();
115 |
116 | if (_contrastColor != null)
117 | {
118 | _contrastColorBrush.Color = _contrastColor.ActiveColor;
119 | _contrastColor.ActiveColorChanged += _contrastColor_ActiveColorChanged;
120 | }
121 |
122 | RecalcContrast();
123 | }
124 | }
125 | }
126 |
127 | private void _contrastColor_ActiveColorChanged(IColorPaletteEntry obj)
128 | {
129 | _contrastColorBrush.Color = obj.ActiveColor;
130 | RecalcContrast();
131 | }
132 |
133 | private SolidColorBrush _contrastColorBrush;
134 | public SolidColorBrush ContrastColorBrush => _contrastColorBrush;
135 |
136 | #region INotifyPropertyChanged
137 |
138 | public event PropertyChangedEventHandler PropertyChanged;
139 |
140 | private void RaisePropertyChanged(string name)
141 | {
142 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
143 | }
144 |
145 | private void RaisePropertyChangedFromSource([CallerMemberName] string name = null)
146 | {
147 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
148 | }
149 |
150 | #endregion
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/EditableColorPaletteEntry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text.Json.Nodes;
7 | using Avalonia.Media;
8 | using FluentEditorShared.Utils;
9 |
10 | namespace FluentEditorShared.ColorPalette
11 | {
12 | // These classes are not intended to be viewmodels.
13 | // They deal with the data about an editable palette and are passed to special purpose controls for editing
14 | public class EditableColorPaletteEntry : IColorPaletteEntry
15 | {
16 | public static EditableColorPaletteEntry Parse(JsonObject data, IColorPaletteEntry sourceColor, IReadOnlyList contrastColors)
17 | {
18 | Color customColor;
19 | if (data.ContainsKey("CustomColor"))
20 | {
21 | customColor = data["CustomColor"].GetColor();
22 | }
23 | else
24 | {
25 | customColor = default(Color);
26 | }
27 | bool useCustomColor = false;
28 | if (data.ContainsKey("UseCustomColor"))
29 | {
30 | useCustomColor = data["UseCustomColor"].GetValue();
31 | }
32 |
33 | ColorStringFormat activeColorStringFormat = ColorStringFormat.PoundRGB;
34 | if (data.ContainsKey("ActiveColorStringFormat"))
35 | {
36 | activeColorStringFormat = data.GetEnum();
37 | }
38 |
39 | return new EditableColorPaletteEntry(sourceColor, customColor, useCustomColor, data.GetOptionalString("Title"), data.GetOptionalString("Description"), activeColorStringFormat, contrastColors);
40 | }
41 |
42 | public EditableColorPaletteEntry(IColorPaletteEntry sourceColor, Color customColor, bool useCustomColor, string title, string description, ColorStringFormat activeColorStringFormat, IReadOnlyList contrastColors)
43 | {
44 | _sourceColor = sourceColor;
45 | _customColor = customColor;
46 | _useCustomColor = useCustomColor;
47 | _title = title;
48 | _description = description;
49 | _activeColorStringFormat = activeColorStringFormat;
50 |
51 | if (_useCustomColor || _sourceColor == null)
52 | {
53 | _activeColor = _customColor;
54 | }
55 | else
56 | {
57 | _activeColor = _sourceColor.ActiveColor;
58 | }
59 |
60 | ContrastColors = contrastColors;
61 | }
62 |
63 | private string _title;
64 | public string Title
65 | {
66 | get => _title;
67 | set => _title = value;
68 | }
69 |
70 | private string _description;
71 | public string Description
72 | {
73 | get => _description;
74 | set => _description = value;
75 | }
76 |
77 | private IColorPaletteEntry _sourceColor;
78 | public IColorPaletteEntry SourceColor
79 | {
80 | get => _sourceColor;
81 | set
82 | {
83 | if (_sourceColor != null)
84 | {
85 | _sourceColor.ActiveColorChanged -= _sourceColor_ActiveColorChanged;
86 | }
87 |
88 | _sourceColor = value;
89 |
90 | if (_sourceColor != null)
91 | {
92 | _sourceColor.ActiveColorChanged += _sourceColor_ActiveColorChanged;
93 | }
94 |
95 | CheckActiveColor();
96 | }
97 | }
98 |
99 | private void _sourceColor_ActiveColorChanged(IColorPaletteEntry obj)
100 | {
101 | CheckActiveColor();
102 | }
103 |
104 | private Color _customColor;
105 | public Color CustomColor
106 | {
107 | get => _customColor;
108 | set
109 | {
110 | if (_customColor != value)
111 | {
112 | _customColor = value;
113 | CustomColorChanged?.Invoke(this);
114 |
115 | CheckActiveColor();
116 | }
117 | }
118 | }
119 |
120 | public event Action CustomColorChanged;
121 |
122 | private bool _useCustomColor;
123 | public bool UseCustomColor
124 | {
125 | get => _useCustomColor;
126 | set
127 | {
128 | if (_useCustomColor != value)
129 | {
130 | _useCustomColor = value;
131 |
132 | CheckActiveColor();
133 | }
134 | }
135 | }
136 |
137 | private void CheckActiveColor()
138 | {
139 | Color newVal;
140 |
141 | if (_useCustomColor || _sourceColor == null)
142 | {
143 | newVal = _customColor;
144 | }
145 | else
146 | {
147 | newVal = _sourceColor.ActiveColor;
148 | }
149 |
150 | if (newVal != _activeColor)
151 | {
152 | _activeColor = newVal;
153 | ActiveColorChanged?.Invoke(this);
154 |
155 | UpdateContrastColor();
156 | }
157 | }
158 |
159 | private Color _activeColor;
160 | public Color ActiveColor
161 | {
162 | get => _activeColor;
163 | set
164 | {
165 | CustomColor = value;
166 | if (_sourceColor != null)
167 | {
168 | UseCustomColor = _customColor != _sourceColor.ActiveColor;
169 | }
170 | }
171 | }
172 |
173 | public string ActiveColorString => ColorUtils.FormatColorString(_activeColor, _activeColorStringFormat);
174 |
175 | private ColorStringFormat _activeColorStringFormat = ColorStringFormat.PoundRGB;
176 | public ColorStringFormat ActiveColorStringFormat => _activeColorStringFormat;
177 |
178 | public event Action ActiveColorChanged;
179 |
180 | private IReadOnlyList _contrastColors;
181 | public IReadOnlyList ContrastColors
182 | {
183 | get => _contrastColors;
184 | set
185 | {
186 | if (_contrastColors != value)
187 | {
188 | if (_contrastColors != null)
189 | {
190 | foreach (var c in _contrastColors)
191 | {
192 | c.Color.ActiveColorChanged -= ContrastColor_ActiveColorChanged;
193 | }
194 | }
195 |
196 | _contrastColors = value;
197 |
198 | if (_contrastColors != null)
199 | {
200 | foreach (var c in _contrastColors)
201 | {
202 | c.Color.ActiveColorChanged += ContrastColor_ActiveColorChanged;
203 | }
204 | }
205 |
206 | UpdateContrastColor();
207 | }
208 | }
209 | }
210 |
211 | private void ContrastColor_ActiveColorChanged(IColorPaletteEntry obj)
212 | {
213 | UpdateContrastColor();
214 | }
215 |
216 | private ContrastColorWrapper _bestContrastColor;
217 | public ContrastColorWrapper BestContrastColor => _bestContrastColor;
218 |
219 | public double BestContrastValue
220 | {
221 | get
222 | {
223 | if (_bestContrastColor == null)
224 | {
225 | return 0;
226 | }
227 | return ColorUtils.ContrastRatio(ActiveColor, _bestContrastColor.Color.ActiveColor);
228 | }
229 | }
230 |
231 | private void UpdateContrastColor()
232 | {
233 | ContrastColorWrapper newContrastColor = null;
234 |
235 | if (_contrastColors != null && _contrastColors.Count > 0)
236 | {
237 | double maxContrast = -1;
238 | foreach (var c in _contrastColors)
239 | {
240 | double contrast = ColorUtils.ContrastRatio(ActiveColor, c.Color.ActiveColor);
241 | if (contrast > maxContrast)
242 | {
243 | maxContrast = contrast;
244 | newContrastColor = c;
245 | }
246 | }
247 | }
248 |
249 | if (_bestContrastColor != newContrastColor)
250 | {
251 | _bestContrastColor = newContrastColor;
252 | ContrastColorChanged?.Invoke(this);
253 | }
254 | }
255 |
256 | public event Action ContrastColorChanged;
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ExpandedColorPaletteEntryEditor.axaml:
--------------------------------------------------------------------------------
1 |
5 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
34 |
38 |
44 |
51 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
76 |
84 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
104 |
105 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/ExpandedColorPaletteEntryEditor.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.Collections.Generic;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Interactivity;
8 | using Avalonia.Media;
9 |
10 | namespace FluentEditorShared.ColorPalette
11 | {
12 | public partial class ExpandedColorPaletteEntryEditor : UserControl
13 | {
14 | public ExpandedColorPaletteEntryEditor()
15 | {
16 | InitializeComponent();
17 | }
18 |
19 | #region ColorPaletteEntryProperty
20 |
21 | public static readonly StyledProperty ColorPaletteEntryProperty = AvaloniaProperty.Register("ColorPaletteEntry");
22 |
23 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
24 | {
25 | base.OnPropertyChanged(change);
26 |
27 | if (change.Property == ColorPaletteEntryProperty)
28 | {
29 | var (oldValue, newValue) = change.GetOldAndNewValue();
30 | OnColorPaletteEntryChanged(oldValue, newValue);
31 | }
32 | }
33 |
34 | private void OnColorPaletteEntryChanged(IColorPaletteEntry oldValue, IColorPaletteEntry newValue)
35 | {
36 | if (oldValue != null)
37 | {
38 | oldValue.ActiveColorChanged -= ColorPaletteEntry_ActiveColorChanged;
39 | }
40 |
41 | if (newValue != null)
42 | {
43 | ColorPicker.Color = newValue.ActiveColor;
44 | newValue.ActiveColorChanged += ColorPaletteEntry_ActiveColorChanged;
45 |
46 | if (string.IsNullOrEmpty(newValue.Title))
47 | {
48 | TitleField.IsVisible = false;
49 | TitleField.Text = string.Empty;
50 | }
51 | else
52 | {
53 | TitleField.IsVisible = true;
54 | TitleField.Text = newValue.Title;
55 | }
56 | if (string.IsNullOrEmpty(newValue.Description))
57 | {
58 | DescriptionField.IsVisible = false;
59 | DescriptionField.Text = string.Empty;
60 | }
61 | else
62 | {
63 | DescriptionField.IsVisible = true;
64 | DescriptionField.Text = newValue.Description;
65 | }
66 | if (newValue is EditableColorPaletteEntry editableNewValue)
67 | {
68 | RevertButton.IsVisible = true;
69 | RevertButton.IsEnabled = editableNewValue.UseCustomColor;
70 | }
71 | else
72 | {
73 | RevertButton.IsVisible = false;
74 | }
75 |
76 | if (newValue.ContrastColors != null)
77 | {
78 | List contrastList = new List();
79 |
80 | foreach (var c in newValue.ContrastColors)
81 | {
82 | if (c.ShowInContrastList)
83 | {
84 | if (c.ShowContrastErrors)
85 | {
86 | contrastList.Add(new ContrastListItem(newValue, c.Color));
87 | }
88 | else
89 | {
90 | contrastList.Add(new ContrastListItem(newValue, c.Color, 0));
91 | }
92 |
93 | }
94 | }
95 |
96 | SetValue(ContrastListProperty, contrastList);
97 | }
98 | else
99 | {
100 | SetValue(ContrastListProperty, null);
101 | }
102 | }
103 | else
104 | {
105 | ColorPicker.Color = default(Color);
106 | SetValue(ContrastListProperty, null);
107 | }
108 | }
109 |
110 | public IColorPaletteEntry ColorPaletteEntry
111 | {
112 | get => GetValue(ColorPaletteEntryProperty);
113 | set => SetValue(ColorPaletteEntryProperty, value);
114 | }
115 |
116 | #endregion
117 |
118 | #region ContrastListProperty
119 |
120 | public static readonly StyledProperty> ContrastListProperty = AvaloniaProperty.Register>("ContrastList");
121 |
122 | public List ContrastList => GetValue(ContrastListProperty);
123 |
124 | #endregion
125 |
126 | private void ColorPaletteEntry_ActiveColorChanged(IColorPaletteEntry obj)
127 | {
128 | if (obj is EditableColorPaletteEntry editablePaletteEntry)
129 | {
130 | RevertButton.IsEnabled = editablePaletteEntry.UseCustomColor;
131 | }
132 | if (obj != null)
133 | {
134 | ColorPicker.Color = obj.ActiveColor;
135 | }
136 | }
137 |
138 | private void ColorPicker_ColorChanged(object sender, ColorChangedEventArgs args)
139 | {
140 | // In this case it is easier to deal with an event than the loopbacks that the ColorPicker creates with a two way binding
141 | var paletteEntry = ColorPaletteEntry;
142 | if (paletteEntry != null)
143 | {
144 | paletteEntry.ActiveColor = args.NewColor;
145 | }
146 | }
147 |
148 | private void RevertButton_Click(object sender, RoutedEventArgs e)
149 | {
150 | var paletteEntry = ColorPaletteEntry;
151 | if (paletteEntry is EditableColorPaletteEntry editablePaletteEntry)
152 | {
153 | editablePaletteEntry.UseCustomColor = false;
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/IColorPalette.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.Collections.Generic;
5 |
6 | namespace FluentEditorShared.ColorPalette
7 | {
8 | // These classes are not intended to be viewmodels.
9 | // They deal with the data about an editable palette and are passed to special purpose controls for editing
10 | public interface IColorPalette
11 | {
12 | IColorPaletteEntry BaseColor { get; }
13 | int Steps { get; }
14 | IReadOnlyList Palette { get; }
15 | IReadOnlyList ContrastColors { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/FluentEditorShared/ColorPalette/IColorPaletteEntry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using Avalonia.Media;
7 | using FluentEditorShared.Utils;
8 |
9 | namespace FluentEditorShared.ColorPalette
10 | {
11 | // These classes are not intended to be viewmodels.
12 | // They deal with the data about an editable palette and are passed to special purpose controls for editing
13 | public interface IColorPaletteEntry
14 | {
15 | string Title { get; }
16 | string Description { get; }
17 |
18 | Color ActiveColor
19 | {
20 | get;
21 | set;
22 | }
23 | string ActiveColorString { get; }
24 | ColorStringFormat ActiveColorStringFormat { get; }
25 |
26 | event Action ActiveColorChanged;
27 |
28 | IReadOnlyList ContrastColors { get; set; }
29 | ContrastColorWrapper BestContrastColor { get; }
30 |
31 | event Action ContrastColorChanged;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ColorMapping.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Text.Json.Nodes;
7 | using Avalonia.Themes.Fluent;
8 | using FluentEditorShared.ColorPalette;
9 | using FluentEditorShared.Utils;
10 |
11 | namespace FluentEditor.ControlPalette
12 | {
13 | public enum ColorTarget { Accent, ErrorText, AltHigh, AltLow, AltMedium, AltMediumHigh, AltMediumLow, BaseHigh, BaseLow, BaseMedium, BaseMediumHigh, BaseMediumLow, ChromeAltLow, ChromeBlackHigh, ChromeBlackLow, ChromeBlackMedium, ChromeBlackMediumLow, ChromeDisabledHigh, ChromeDisabledLow, ChromeGray, ChromeHigh, ChromeLow, ChromeMedium, ChromeMediumLow, ChromeWhite, ListLow, ListMedium }
14 | public enum ColorSource { LightRegion, DarkRegion, LightBase, DarkBase, LightPrimary, DarkPrimary, White, Black }
15 |
16 | public class ColorMapping
17 | {
18 | public static ColorMapping Parse(JsonObject data, IColorPaletteEntry lightRegion, IColorPaletteEntry darkRegion, ColorPalette lightBase, ColorPalette darkBase, ColorPalette lightPrimary, ColorPalette darkPrimary, IColorPaletteEntry white, IColorPaletteEntry black)
19 | {
20 | var target = data["Target"].GetEnum();
21 | var source = data["Source"].GetEnum();
22 | int index = 0;
23 | if (data.ContainsKey("SourceIndex"))
24 | {
25 | index = data["SourceIndex"].GetInt();
26 | }
27 |
28 | switch (source)
29 | {
30 | case ColorSource.LightRegion:
31 | return new ColorMapping(lightRegion, target);
32 | case ColorSource.DarkRegion:
33 | return new ColorMapping(darkRegion, target);
34 | case ColorSource.LightBase:
35 | return new ColorMapping(lightBase.Palette[index], target);
36 | case ColorSource.DarkBase:
37 | return new ColorMapping(darkBase.Palette[index], target);
38 | case ColorSource.LightPrimary:
39 | return new ColorMapping(lightPrimary.Palette[index], target);
40 | case ColorSource.DarkPrimary:
41 | return new ColorMapping(darkPrimary.Palette[index], target);
42 | case ColorSource.White:
43 | return new ColorMapping(white, target);
44 | case ColorSource.Black:
45 | return new ColorMapping(black, target);
46 | }
47 |
48 | return null;
49 | }
50 |
51 | public static List ParseList(JsonArray data, IColorPaletteEntry lightRegion, IColorPaletteEntry darkRegion, ColorPalette lightBase, ColorPalette darkBase, ColorPalette lightPrimary, ColorPalette darkPrimary, IColorPaletteEntry white, IColorPaletteEntry black)
52 | {
53 | List retVal = new List();
54 | foreach (var node in data)
55 | {
56 | retVal.Add(Parse(node.AsObject(), lightRegion, darkRegion, lightBase, darkBase, lightPrimary, darkPrimary, white, black));
57 | }
58 | return retVal;
59 | }
60 |
61 | public ColorMapping(IColorPaletteEntry source, ColorTarget targetColor)
62 | {
63 | _source = source;
64 | _targetColor = targetColor;
65 | }
66 |
67 | private readonly IColorPaletteEntry _source;
68 | public IColorPaletteEntry Source => _source;
69 |
70 | private readonly ColorTarget _targetColor;
71 | public ColorTarget Target => _targetColor;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ControlPaletteTestContent.axaml:
--------------------------------------------------------------------------------
1 |
5 |
18 |
19 |
20 |
21 |
26 |
27 |
31 |
32 |
37 |
38 |
42 |
46 |
51 |
55 |
56 |
60 |
64 |
69 |
73 |
77 |
82 |
88 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
103 |
109 |
113 |
114 |
115 |
116 |
122 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
143 |
147 |
148 |
152 |
153 |
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ControlPaletteTestContent.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using Avalonia;
5 | using Avalonia.Controls;
6 |
7 | namespace FluentEditor.ControlPalette
8 | {
9 | public sealed partial class ControlPaletteTestContent : UserControl
10 | {
11 | public ControlPaletteTestContent()
12 | {
13 | InitializeComponent();
14 | }
15 |
16 | public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register("Title");
17 |
18 | public string Title
19 | {
20 | get => GetValue(TitleProperty);
21 | set => SetValue(TitleProperty, value);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ControlPaletteView.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia;
6 | using Avalonia.Controls;
7 | using Avalonia.Styling;
8 | using Avalonia.Themes.Fluent;
9 |
10 | namespace FluentEditor.ControlPalette
11 | {
12 | public sealed partial class ControlPaletteView : UserControl
13 | {
14 | public ControlPaletteView()
15 | {
16 | InitializeComponent();
17 | }
18 |
19 | #region ViewModelProperty
20 |
21 | public static readonly StyledProperty ViewModelProperty = AvaloniaProperty.Register("ViewModel");
22 |
23 | public ControlPaletteViewModel ViewModel
24 | {
25 | get => GetValue(ViewModelProperty);
26 | set => SetValue(ViewModelProperty, value);
27 | }
28 |
29 | #endregion
30 |
31 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
32 | {
33 | base.OnPropertyChanged(change);
34 |
35 | if (change.Property == ViewModelProperty)
36 | {
37 | ViewModel.RebuldPreviews += ViewModelOnRebuldPreviews;
38 | ViewModelOnRebuldPreviews(this, EventArgs.Empty);
39 | }
40 | }
41 |
42 | private void ViewModelOnRebuldPreviews(object sender, EventArgs e)
43 | {
44 | TestContentContainer.Styles.Clear();
45 | TestContentContainer.Styles.Add(new FluentTheme
46 | {
47 | Palettes =
48 | {
49 | [ThemeVariant.Default] = ViewModel.CreateResources(false),
50 | [ThemeVariant.Light] = ViewModel.CreateResources(false),
51 | [ThemeVariant.Dark] = ViewModel.CreateResources(true)
52 | }
53 | });
54 |
55 | LightTestContentContainer.Child = new ControlPaletteTestContent
56 | {
57 | Title = App.StringProvider.GetString("ControlPaletteLightTestContent")
58 | };
59 | DarkTestContentContainer.Child = new ControlPaletteTestContent
60 | {
61 | Title = App.StringProvider.GetString("ControlPaletteDarkTestContent")
62 | };
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ControlPaletteViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.ComponentModel;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Runtime.CompilerServices;
10 | using System.Text.Json.Nodes;
11 | using System.Threading.Tasks;
12 | using Avalonia;
13 | using Avalonia.Controls;
14 | using Avalonia.Media;
15 | using Avalonia.Platform.Storage;
16 | using Avalonia.Themes.Fluent;
17 | using FluentEditor.ControlPalette.Model;
18 | using FluentEditor.OuterNav;
19 | using FluentEditorShared;
20 | using FluentEditorShared.ColorPalette;
21 | using FluentEditorShared.Utils;
22 |
23 | namespace FluentEditor.ControlPalette
24 | {
25 | public class ControlPaletteViewModel : INavItem
26 | {
27 | public static ControlPaletteViewModel Parse(IStringProvider stringProvider, JsonObject data, IControlPaletteModel paletteModel, IControlPaletteExportProvider exportProvider)
28 | {
29 | return new ControlPaletteViewModel(stringProvider, paletteModel, data["Id"].GetOptionalString(), data["Title"].GetOptionalString(), exportProvider);
30 | }
31 |
32 | public ControlPaletteViewModel(IStringProvider stringProvider, IControlPaletteModel paletteModel, string id, string title, IControlPaletteExportProvider exportProvider)
33 | {
34 | _stringProvider = stringProvider;
35 | _id = id;
36 | _title = title;
37 |
38 | _paletteModel = paletteModel;
39 | _exportProvider = exportProvider;
40 |
41 | _paletteModel.PaletteUpdated += PaletteModelOnPaletteUpdated;
42 | _paletteModel.ActivePresetChanged += OnActivePresetChanged;
43 | }
44 |
45 | public event EventHandler RebuldPreviews;
46 |
47 | private IStringProvider _stringProvider;
48 |
49 | private readonly string _id;
50 | public string Id => _id;
51 |
52 | private string _title;
53 | public string Title
54 | {
55 | get => _title;
56 | set
57 | {
58 | if (_title != value)
59 | {
60 | _title = value;
61 | RaisePropertyChangedFromSource();
62 | }
63 | }
64 | }
65 |
66 | public void OnSaveDataRequested()
67 | {
68 | _ = SaveData();
69 | }
70 |
71 | private async Task SaveData()
72 | {
73 | var provider = TopLevel.GetTopLevel(App.NavPage).StorageProvider;
74 | var file = await provider.SaveFilePickerAsync(new FilePickerSaveOptions
75 | {
76 | FileTypeChoices = new[] { App.JsonFileType }
77 | });
78 |
79 | if (file == null)
80 | {
81 | return;
82 | }
83 |
84 | Preset savePreset = new Preset(file.Name, file.Name, _paletteModel);
85 | var saveData = Preset.Serialize(savePreset);
86 | var saveString = saveData.ToString();
87 |
88 | await using var stream = await file.OpenWriteAsync();
89 | await using var writer = new StreamWriter(stream);
90 | await writer.WriteAsync(saveString);
91 | }
92 |
93 | public void OnLoadDataRequested()
94 | {
95 | _ = LoadData();
96 | }
97 |
98 | private async Task LoadData()
99 | {
100 | var provider = TopLevel.GetTopLevel(App.NavPage).StorageProvider;
101 | var file = (await provider.OpenFilePickerAsync(new FilePickerOpenOptions
102 | {
103 | FileTypeFilter = new[] { App.JsonFileType }
104 | })).FirstOrDefault();
105 |
106 | if (file == null)
107 | {
108 | return;
109 | }
110 |
111 | await using var data = await file.OpenReadAsync();
112 | using var reader = new StreamReader(data);
113 | var rootObject = JsonObject.Parse(await reader.ReadToEndAsync()).AsObject();
114 | Preset loadedPreset = null;
115 | try
116 | {
117 | loadedPreset = Preset.Parse(rootObject, file.TryGetLocalPath() ?? file.Name, file.Name);
118 | }
119 | catch
120 | {
121 | loadedPreset = null;
122 | }
123 |
124 | _paletteModel.AddOrReplacePreset(loadedPreset);
125 | _paletteModel.ApplyPreset(loadedPreset);
126 | }
127 |
128 | private void PaletteModelOnPaletteUpdated(object sender, EventArgs e)
129 | {
130 | RebuldPreviews?.Invoke(this, e);
131 | }
132 |
133 | private void OnActivePresetChanged(IControlPaletteModel obj)
134 | {
135 | RaisePropertyChanged("ActivePreset");
136 | }
137 |
138 | private readonly IControlPaletteModel _paletteModel;
139 | private readonly IControlPaletteExportProvider _exportProvider;
140 |
141 | public Preset ActivePreset
142 | {
143 | get => _paletteModel.ActivePreset;
144 | set => _paletteModel.ApplyPreset(value);
145 | }
146 |
147 | public IReadOnlyList Presets => _paletteModel.Presets;
148 |
149 | public ColorPaletteEntry LightRegion => _paletteModel.LightRegion;
150 |
151 | public ColorPaletteEntry DarkRegion => _paletteModel.DarkRegion;
152 |
153 | public ColorPalette LightBase => _paletteModel.LightBase;
154 |
155 | public ColorPalette DarkBase => _paletteModel.DarkBase;
156 |
157 | public ColorPalette LightPrimary => _paletteModel.LightPrimary;
158 |
159 | public ColorPalette DarkPrimary => _paletteModel.DarkPrimary;
160 |
161 | public IReadOnlyList LightColorMapping => _paletteModel.LightColorMapping;
162 |
163 | public IReadOnlyList DarkColorMapping => _paletteModel.DarkColorMapping;
164 |
165 | public void OnExportRequested()
166 | {
167 | _exportProvider.ShowExportView(_exportProvider.GenerateExportData(_paletteModel, this));
168 | }
169 |
170 | public ColorPaletteResources CreateResources(bool dark)
171 | {
172 | var res = new ColorPaletteResources();
173 | res.RegionColor = dark ? DarkRegion.ActiveColor : LightRegion.ActiveColor;
174 | res.Accent = GetColor(ColorTarget.Accent);
175 | res.ErrorText = GetColor(ColorTarget.ErrorText);
176 | res.AltHigh = GetColor(ColorTarget.AltHigh);
177 | res.AltLow = GetColor(ColorTarget.AltLow);
178 | res.AltMedium = GetColor(ColorTarget.AltMedium);
179 | res.AltMediumHigh = GetColor(ColorTarget.AltMediumHigh);
180 | res.AltMediumLow = GetColor(ColorTarget.AltMediumLow);
181 | res.BaseHigh = GetColor(ColorTarget.BaseHigh);
182 | res.BaseLow = GetColor(ColorTarget.BaseLow);
183 | res.BaseMedium = GetColor(ColorTarget.BaseMedium);
184 | res.BaseMediumHigh = GetColor(ColorTarget.BaseMediumHigh);
185 | res.BaseMediumLow = GetColor(ColorTarget.BaseMediumLow);
186 | res.ChromeAltLow = GetColor(ColorTarget.ChromeAltLow);
187 | res.ChromeBlackHigh = GetColor(ColorTarget.ChromeBlackHigh);
188 | res.ChromeBlackLow = GetColor(ColorTarget.ChromeBlackLow);
189 | res.ChromeBlackMedium = GetColor(ColorTarget.ChromeBlackMedium);
190 | res.ChromeBlackMediumLow = GetColor(ColorTarget.ChromeBlackMediumLow);
191 | res.ChromeDisabledHigh = GetColor(ColorTarget.ChromeDisabledHigh);
192 | res.ChromeDisabledLow = GetColor(ColorTarget.ChromeDisabledLow);
193 | res.ChromeGray = GetColor(ColorTarget.ChromeGray);
194 | res.ChromeHigh = GetColor(ColorTarget.ChromeHigh);
195 | res.ChromeLow = GetColor(ColorTarget.ChromeLow);
196 | res.ChromeMedium = GetColor(ColorTarget.ChromeMedium);
197 | res.ChromeMediumLow = GetColor(ColorTarget.ChromeMediumLow);
198 | res.ChromeWhite = GetColor(ColorTarget.ChromeWhite);
199 | res.ListLow = GetColor(ColorTarget.ListLow);
200 | res.ListMedium = GetColor(ColorTarget.ListMedium);
201 | return res;
202 |
203 | Color GetColor(ColorTarget target)
204 | {
205 | var mappings = dark ? DarkColorMapping : LightColorMapping;
206 | return mappings.FirstOrDefault(m => m.Target == target)?.Source.ActiveColor ?? default;
207 | }
208 | }
209 |
210 | #region INotifyPropertyChanged
211 |
212 | public event PropertyChangedEventHandler PropertyChanged;
213 |
214 | private void RaisePropertyChanged(string name)
215 | {
216 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
217 | }
218 |
219 | private void RaisePropertyChangedFromSource([CallerMemberName] string name = null)
220 | {
221 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
222 | }
223 |
224 | #endregion
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ExportView.axaml:
--------------------------------------------------------------------------------
1 |
5 |
18 |
19 |
20 |
21 |
26 |
27 |
34 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ExportView.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia.Controls;
6 | using Avalonia.Interactivity;
7 |
8 | namespace FluentEditor.ControlPalette.Export
9 | {
10 | public partial class ExportView : UserControl
11 | {
12 | public ExportView(ExportViewModel viewModel)
13 | {
14 | if (viewModel == null)
15 | {
16 | throw new ArgumentNullException("viewModel");
17 | }
18 | _viewModel = viewModel;
19 | InitializeComponent();
20 | }
21 |
22 | private readonly ExportViewModel _viewModel;
23 | public ExportViewModel ViewModel => _viewModel;
24 |
25 | private void ExportClick(object sender, RoutedEventArgs e)
26 | {
27 | TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(ViewModel.ExportText);
28 | }
29 |
30 | private void LearnMoreClick(object sender, RoutedEventArgs e)
31 | {
32 | throw new NotImplementedException();
33 | }
34 |
35 | private void Close(object sender, RoutedEventArgs e)
36 | {
37 | ((ContentControl)Parent!).Content = null;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/ExportViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.ComponentModel;
5 | using System.Runtime.CompilerServices;
6 |
7 | namespace FluentEditor.ControlPalette.Export
8 | {
9 | public class ExportViewModel : INotifyPropertyChanged
10 | {
11 | public ExportViewModel()
12 | : this(null) { }
13 |
14 | public ExportViewModel(string exportText)
15 | {
16 | _exportText = exportText;
17 | }
18 |
19 | private string _exportText;
20 | public string ExportText
21 | {
22 | get => _exportText;
23 | set
24 | {
25 | if(_exportText != value)
26 | {
27 | _exportText = value;
28 | RaisePropertyChangedFromSource();
29 | RaisePropertyChanged("ReadyToCopy");
30 | }
31 | }
32 | }
33 |
34 | public bool ReadyToCopy => !string.IsNullOrEmpty(_exportText);
35 |
36 | #region INotifyPropertyChanged
37 |
38 | public event PropertyChangedEventHandler PropertyChanged;
39 |
40 | private void RaisePropertyChanged(string name)
41 | {
42 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
43 | }
44 |
45 | private void RaisePropertyChangedFromSource([CallerMemberName] string name = null)
46 | {
47 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
48 | }
49 |
50 | #endregion
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/FluentEditorShared/ControlPalette/Model/ControlPaletteExportProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using FluentEditor.ControlPalette.Export;
7 | using FluentEditorShared.ColorPalette;
8 |
9 | namespace FluentEditor.ControlPalette.Model
10 | {
11 | public interface IControlPaletteExportProvider
12 | {
13 | Task ShowExportView(string exportData);
14 |
15 | string GenerateExportData(IControlPaletteModel model, ControlPaletteViewModel viewModel,
16 | bool showAllColors = false);
17 | }
18 |
19 | public class ControlPaletteExportProvider : IControlPaletteExportProvider
20 | {
21 | private bool _isWindowInitializing = false;
22 |
23 | // This is owned by the UI thread for the _exportWindow
24 | private ExportViewModel _exportViewModel;
25 |
26 | public string GenerateExportData(IControlPaletteModel model, ControlPaletteViewModel viewModel,
27 | bool showAllColors = false)
28 | {
29 | StringBuilder sb = new StringBuilder();
30 |
31 | sb.AppendLine("");
32 | sb.AppendLine(" ");
33 | sb.Append(" ");
44 | sb.Append(" ");
55 |
56 | sb.AppendLine(" ");
57 | sb.AppendLine("");
58 |
59 | var retVal = sb.ToString();
60 | return retVal;
61 |
62 | void AppendColor(string name, IColorPaletteEntry entry)
63 | {
64 | sb.Append(" ");
65 | sb.Append(name);
66 | sb.Append("=\"");
67 | sb.Append(entry.ActiveColor.ToString());
68 | sb.Append("\"");
69 | }
70 | }
71 |
72 | public async Task ShowExportView(string exportData)
73 | {
74 | _exportViewModel = new ExportViewModel(exportData);
75 | var exportView = new ExportView(_exportViewModel);
76 | exportView.DataContext = _exportViewModel;
77 | App.NavPage.OverlayContainer.Content = exportView;
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/FluentEditorShared/Converters.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Globalization;
6 | using Avalonia.Data.Converters;
7 | using Avalonia.Media;
8 |
9 | namespace FluentEditorShared
10 | {
11 | public class ColorToStringConverter : IValueConverter
12 | {
13 | public object Convert(object value, Type targetType, object parameter, CultureInfo language)
14 | {
15 | if(value is Color c)
16 | {
17 | return c.ToString();
18 | }
19 |
20 | return null;
21 | }
22 |
23 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo language)
24 | {
25 | try
26 | {
27 | if (Color.TryParse(value as string, out var c))
28 | {
29 | return c;
30 | }
31 |
32 | return Colors.White;
33 | }
34 | catch
35 | {
36 | return Colors.White;
37 | }
38 | }
39 | }
40 |
41 | public class NullableColorToStringConverter : IValueConverter
42 | {
43 | public object Convert(object value, Type targetType, object parameter, CultureInfo language)
44 | {
45 | if (value is Color?)
46 | {
47 | var c = (Color?)value;
48 | if(c.HasValue)
49 | {
50 | return c.Value.ToString();
51 | }
52 |
53 | return null;
54 | }
55 |
56 | return null;
57 | }
58 |
59 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo language)
60 | {
61 | try
62 | {
63 | if (Color.TryParse(value as string, out var c))
64 | {
65 | return c;
66 | }
67 |
68 | return Colors.White;
69 | }
70 | catch
71 | {
72 | return Colors.White;
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/FluentEditorShared/FluentEditorShared.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net7.0
4 | en-US
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | true
24 | true
25 | true
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/FluentEditorShared/FluentEditorSharedResources.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | 0 0 0 0
11 | 0
12 |
13 | #FF1C1C1C
14 | #FF1C1C1C
15 |
16 |
17 |
18 |
19 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 0 0 0 0
30 | 0
31 |
32 |
33 |
34 | #FFFFFFFF
35 | #FFFFFFFF
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | #ff0073CF
48 | #FF2B2B2B
49 | #FF2B2B2B
50 |
51 |
53 |
55 |
57 |
58 |
60 |
62 |
63 |
64 |
65 |
66 |
67 |
69 |
71 |
73 |
74 | 2
75 | 4
76 |
77 | 1
78 | 1
79 | 1
80 | 1
81 | 1
82 | 1
83 | 1
84 |
85 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
103 |
108 |
114 |
115 |
116 |
117 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
150 |
159 |
160 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
195 |
196 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/FluentEditorShared/LocExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia.Markup.Xaml;
3 | using FluentEditor;
4 |
5 | namespace FluentEditorShared;
6 |
7 | public class LocExtension : MarkupExtension
8 | {
9 | private readonly string _key;
10 |
11 | public LocExtension(string key)
12 | {
13 | _key = key;
14 | }
15 |
16 | public override object ProvideValue(IServiceProvider serviceProvider)
17 | {
18 | return App.StringProvider.GetString(_key);
19 | }
20 | }
--------------------------------------------------------------------------------
/FluentEditorShared/Model/MainNavModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text.Json.Nodes;
8 | using System.Threading.Tasks;
9 | using Avalonia.Platform;
10 | using FluentEditor.ControlPalette;
11 | using FluentEditor.ControlPalette.Model;
12 | using FluentEditor.OuterNav;
13 | using FluentEditorShared;
14 | using FluentEditorShared.Utils;
15 |
16 | namespace FluentEditor.Model
17 | {
18 | public interface IMainNavModel
19 | {
20 | Task InitializeData(string dataPath, IControlPaletteModel paletteModel, ControlPaletteExportProvider controlPaletteExportProvider);
21 | Task HandleAppSuspend();
22 |
23 | IReadOnlyList NavItems { get; }
24 | INavItem DefaultNavItem { get; }
25 | }
26 |
27 | public class MainNavModel : IMainNavModel
28 | {
29 | public MainNavModel(IStringProvider stringProvider)
30 | {
31 | _stringProvider = stringProvider;
32 | }
33 |
34 | private IStringProvider _stringProvider;
35 |
36 | public async Task InitializeData(string dataPath, IControlPaletteModel paletteModel, ControlPaletteExportProvider controlPaletteExportProvider)
37 | {
38 | var asset = AssetLoader.Open(new Uri(dataPath));
39 | var rootObject = JsonObject.Parse(asset).AsObject();
40 |
41 | List navItems = new List();
42 |
43 | if (rootObject.ContainsKey("Demos"))
44 | {
45 | JsonArray demoDataList = rootObject["Demos"].AsArray();
46 | foreach (var demoData in demoDataList)
47 | {
48 | navItems.Add(ParseNavItem(demoData.AsObject(), paletteModel, controlPaletteExportProvider));
49 | }
50 | }
51 |
52 | string defaultDemoId = rootObject.GetOptionalString("DefaultDemoId");
53 | if (!string.IsNullOrEmpty(defaultDemoId))
54 | {
55 | _defaultNavItem = navItems.FirstOrDefault(a => a.Id == defaultDemoId);
56 | }
57 |
58 | _navItems = navItems;
59 | }
60 |
61 | public Task HandleAppSuspend()
62 | {
63 | // Currently nothing to do here
64 | return Task.CompletedTask;
65 | }
66 |
67 | private INavItem ParseNavItem(JsonObject data, IControlPaletteModel paletteModel, ControlPaletteExportProvider controlPaletteExportProvider)
68 | {
69 | string type = data.GetOptionalString("Type");
70 |
71 | switch (type)
72 | {
73 | case "ControlPalette":
74 | return ControlPaletteViewModel.Parse(_stringProvider, data, paletteModel, controlPaletteExportProvider);
75 | default:
76 | throw new Exception(string.Format("Unknown nav item type {0}", type));
77 | }
78 | }
79 |
80 | private List _navItems;
81 | public IReadOnlyList NavItems => _navItems;
82 |
83 | private INavItem _defaultNavItem;
84 | public INavItem DefaultNavItem => _defaultNavItem;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/FluentEditorShared/OuterNav/INavItem.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System.ComponentModel;
5 |
6 | namespace FluentEditor.OuterNav
7 | {
8 | public interface INavItem : INotifyPropertyChanged
9 | {
10 | string Id { get; }
11 | string Title { get; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/FluentEditorShared/OuterNav/OuterNavPage.axaml:
--------------------------------------------------------------------------------
1 |
5 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/FluentEditorShared/OuterNav/OuterNavPage.axaml.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia.Controls;
6 |
7 | namespace FluentEditor.OuterNav
8 | {
9 | public sealed partial class OuterNavPage : UserControl
10 | {
11 | public OuterNavPage(OuterNavViewModel viewModel)
12 | {
13 | if (viewModel == null)
14 | {
15 | throw new ArgumentNullException("viewModel");
16 | }
17 | _viewModel = viewModel;
18 | _viewModel.NavigateToItem += _viewModel_NavigateToItem;
19 |
20 | InitializeComponent();
21 |
22 | NavigateToViewModel(_viewModel.SelectedNavItem);
23 | }
24 |
25 | private readonly OuterNavViewModel _viewModel;
26 | public OuterNavViewModel ViewModel => _viewModel;
27 |
28 | private void _viewModel_NavigateToItem(OuterNavViewModel source, INavItem navItem)
29 | {
30 | NavigateToViewModel(navItem);
31 | }
32 |
33 | private void NavigateToViewModel(object viewModel)
34 | {
35 | NavigationFrame.Content = viewModel;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/FluentEditorShared/OuterNav/OuterNavViewModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.ComponentModel;
7 | using System.Runtime.CompilerServices;
8 | using Avalonia.Collections;
9 |
10 | namespace FluentEditor.OuterNav
11 | {
12 | public class OuterNavViewModel : INotifyPropertyChanged
13 | {
14 | public OuterNavViewModel(IReadOnlyList navItems, INavItem selectedNavItem = null)
15 | {
16 | if (navItems == null)
17 | {
18 | throw new ArgumentNullException("navItems");
19 | }
20 | _navItems = new AvaloniaList(navItems);
21 |
22 | if (selectedNavItem != null)
23 | {
24 | if (!_navItems.Contains(selectedNavItem))
25 | {
26 | throw new Exception("selectedNavItem is not contained in navItems");
27 | }
28 | _selectedNavItem = selectedNavItem;
29 | }
30 | }
31 |
32 | private AvaloniaList _navItems;
33 | public AvaloniaList NavItems => _navItems;
34 |
35 | private INavItem _selectedNavItem;
36 | public INavItem SelectedNavItem
37 | {
38 | get => _selectedNavItem;
39 | set
40 | {
41 | if (_selectedNavItem != value)
42 | {
43 | _selectedNavItem = value;
44 | RaisePropertyChangedFromSource();
45 |
46 | NavigateToItem?.Invoke(this, _selectedNavItem);
47 | }
48 | }
49 | }
50 |
51 | public event Action NavigateToItem;
52 |
53 | #region INotifyPropertyChanged
54 |
55 | public event PropertyChangedEventHandler PropertyChanged;
56 |
57 | private void RaisePropertyChanged(string name)
58 | {
59 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
60 | }
61 |
62 | private void RaisePropertyChangedFromSource([CallerMemberName] string name = null)
63 | {
64 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
65 | }
66 |
67 | #endregion
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/FluentEditorShared/Resources/Resources.en-US.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 | text/microsoft-resx
4 |
5 |
6 | 1.3
7 |
8 |
9 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
10 |
11 |
12 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
13 |
14 |
--------------------------------------------------------------------------------
/FluentEditorShared/Resources/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | text/microsoft-resx
8 |
9 |
10 | 1.3
11 |
12 |
13 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
14 |
15 |
16 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
17 |
18 |
19 | Learn more about contrast ratios
20 |
21 |
22 | Contrast Ratio Information
23 |
24 |
25 | Revert Color
26 |
27 |
28 | A tool that helps demonstrate the flexibility of the Fluent Design System as well as supports the app development process by generating XAML markup for our ResourceDictionary framework used in Universal Windows Platform (UWP) applications
29 |
30 |
31 | Fluent XAML Theme Editor
32 |
33 |
34 | Apply
35 |
36 |
37 | Cancel
38 |
39 |
40 | Maps to {0}
41 |
42 |
43 | Border Preview
44 |
45 |
46 | Border Thickness
47 |
48 |
49 | Control Preview
50 |
51 |
52 | Base
53 |
54 |
55 | Dark theme
56 |
57 |
58 | Dark theme
59 |
60 |
61 | avares://FluentEditorShared/Resources/controlPaletteData.json
62 |
63 |
64 | Ok
65 |
66 |
67 | Export
68 |
69 |
70 | Color
71 |
72 |
73 | Light theme
74 |
75 |
76 | Light theme
77 |
78 |
79 | Load
80 |
81 |
82 | Unable to load file {0}
83 |
84 |
85 | Unable to load
86 |
87 |
88 | Custom
89 |
90 |
91 | Primary
92 |
93 |
94 | Region
95 |
96 |
97 | Reset colors
98 |
99 |
100 | Save
101 |
102 |
103 | Unable to save file {0}
104 |
105 |
106 | Unable to save
107 |
108 |
109 | Controls
110 |
111 |
112 | Corner Radius
113 |
114 |
115 | White Text
116 |
117 |
118 | Default
119 |
120 |
121 | Copy to Clipboard
122 |
123 |
124 | Learn more about XAML ResourceDictionary
125 |
126 |
127 | Black Text
128 |
129 |
130 | avares://FluentEditorShared/Resources/demodata.json
131 |
132 |
133 | OK
134 |
135 |
136 | Overlay
137 |
138 |
139 | Overlay Preview
140 |
141 |
142 | No Rounding, Thicker Borders
143 |
144 |
145 | Yolo
146 |
147 |
148 | Buttons
149 |
150 |
151 | Calendar view
152 |
153 |
154 | Checkbox
155 |
156 |
157 | Checked
158 |
159 |
160 | Checked
161 |
162 |
163 | Combo box
164 |
165 |
166 | Item 1
167 |
168 |
169 | Item 2
170 |
171 |
172 | Item 3
173 |
174 |
175 | Command bar
176 |
177 |
178 | Date picker
179 |
180 |
181 | Disabled accent button
182 |
183 |
184 | Disabled button
185 |
186 |
187 | Disabled
188 |
189 |
190 | Disabled
191 |
192 |
193 | Editable item 1
194 |
195 |
196 | Editable item 2
197 |
198 |
199 | Editable item 3
200 |
201 |
202 | Enabled accent button
203 |
204 |
205 | Enabled button
206 |
207 |
208 | List view
209 |
210 |
211 | Howdy! (disabled)
212 |
213 |
214 | Aloha! (selected)
215 |
216 |
217 | Hello!
218 |
219 |
220 | Hola!
221 |
222 |
223 | Radio button
224 |
225 |
226 | Slider
227 |
228 |
229 | Enter text here
230 |
231 |
232 | Textbox
233 |
234 |
235 | Third state
236 |
237 |
238 | Time picker
239 |
240 |
241 | Toggle switch
242 |
243 |
244 | Toggle button
245 |
246 |
247 | Toggled button
248 |
249 |
250 | Unchecked
251 |
252 |
253 | Unchecked
254 |
255 |
--------------------------------------------------------------------------------
/FluentEditorShared/Resources/controlPaletteData.json:
--------------------------------------------------------------------------------
1 | {
2 | "LightRegion": {
3 | "Title": "Light Region",
4 | "Color": "#FFFFFFFF"
5 | },
6 | "DarkRegion": {
7 | "Title": "Dark Region",
8 | "Color": "#FF282828"
9 | },
10 | "LightBase": {
11 | "Steps": 11,
12 | "BaseColor": {
13 | "Title": "Light Base",
14 | "Color": "#FFCCCCCC"
15 | }
16 | },
17 | "DarkBase": {
18 | "Steps": 11,
19 | "BaseColor": {
20 | "Title": "Dark Base",
21 | "Color": "#FF333333"
22 | }
23 | },
24 | "LightPrimary": {
25 | "Steps": 11,
26 | "BaseColor": {
27 | "Title": "Light Primary",
28 | "Color": "#FF1686AE"
29 | }
30 | },
31 | "DarkPrimary": {
32 | "Steps": 11,
33 | "BaseColor": {
34 | "Title": "Dark Primary",
35 | "Color": "#FF1686AE"
36 | }
37 | },
38 | "LightPaletteMapping": [
39 | {
40 | "Target": "Accent",
41 | "Source": "LightPrimary",
42 | "SourceIndex": "5"
43 | },
44 | {
45 | "Target": "AltHigh",
46 | "Source": "White"
47 | },
48 | {
49 | "Target": "AltLow",
50 | "Source": "White"
51 | },
52 | {
53 | "Target": "AltMedium",
54 | "Source": "White"
55 | },
56 | {
57 | "Target": "AltMediumHigh",
58 | "Source": "White"
59 | },
60 | {
61 | "Target": "AltMediumLow",
62 | "Source": "White"
63 | },
64 | {
65 | "Target": "BaseHigh",
66 | "Source": "Black"
67 | },
68 | {
69 | "Target": "BaseLow",
70 | "Source": "LightBase",
71 | "SourceIndex": "5"
72 | },
73 | {
74 | "Target": "BaseMedium",
75 | "Source": "LightBase",
76 | "SourceIndex": "8"
77 | },
78 | {
79 | "Target": "BaseMediumHigh",
80 | "Source": "LightBase",
81 | "SourceIndex": "10"
82 | },
83 | {
84 | "Target": "BaseMediumLow",
85 | "Source": "LightBase",
86 | "SourceIndex": "9"
87 | },
88 | {
89 | "Target": "ChromeAltLow",
90 | "Source": "LightBase",
91 | "SourceIndex": "10"
92 | },
93 | {
94 | "Target": "ChromeBlackHigh",
95 | "Source": "Black"
96 | },
97 | {
98 | "Target": "ChromeBlackLow",
99 | "Source": "LightBase",
100 | "SourceIndex": "5"
101 | },
102 | {
103 | "Target": "ChromeBlackMedium",
104 | "Source": "LightBase",
105 | "SourceIndex": "10"
106 | },
107 | {
108 | "Target": "ChromeBlackMediumLow",
109 | "Source": "LightBase",
110 | "SourceIndex": "8"
111 | },
112 | {
113 | "Target": "ChromeDisabledHigh",
114 | "Source": "LightBase",
115 | "SourceIndex": "5"
116 | },
117 | {
118 | "Target": "ChromeDisabledLow",
119 | "Source": "LightBase",
120 | "SourceIndex": "8"
121 | },
122 | {
123 | "Target": "ChromeGray",
124 | "Source": "LightBase",
125 | "SourceIndex": "9"
126 | },
127 | {
128 | "Target": "ChromeHigh",
129 | "Source": "LightBase",
130 | "SourceIndex": "5"
131 | },
132 | {
133 | "Target": "ChromeLow",
134 | "Source": "LightBase",
135 | "SourceIndex": "0"
136 | },
137 | {
138 | "Target": "ChromeMedium",
139 | "Source": "LightBase",
140 | "SourceIndex": "1"
141 | },
142 | {
143 | "Target": "ChromeMediumLow",
144 | "Source": "LightBase",
145 | "SourceIndex": "0"
146 | },
147 | {
148 | "Target": "ChromeWhite",
149 | "Source": "White"
150 | },
151 | {
152 | "Target": "ListLow",
153 | "Source": "LightBase",
154 | "SourceIndex": "1"
155 | },
156 | {
157 | "Target": "ListMedium",
158 | "Source": "LightBase",
159 | "SourceIndex": "5"
160 | }
161 | ],
162 | "DarkPaletteMapping": [
163 | {
164 | "Target": "Accent",
165 | "Source": "DarkPrimary",
166 | "SourceIndex": "5"
167 | },
168 | {
169 | "Target": "AltHigh",
170 | "Source": "Black"
171 | },
172 | {
173 | "Target": "AltLow",
174 | "Source": "Black"
175 | },
176 | {
177 | "Target": "AltMedium",
178 | "Source": "Black"
179 | },
180 | {
181 | "Target": "AltMediumHigh",
182 | "Source": "Black"
183 | },
184 | {
185 | "Target": "AltMediumLow",
186 | "Source": "Black"
187 | },
188 | {
189 | "Target": "BaseHigh",
190 | "Source": "White"
191 | },
192 | {
193 | "Target": "BaseLow",
194 | "Source": "DarkBase",
195 | "SourceIndex": "5"
196 | },
197 | {
198 | "Target": "BaseMedium",
199 | "Source": "DarkBase",
200 | "SourceIndex": "1"
201 | },
202 | {
203 | "Target": "BaseMediumHigh",
204 | "Source": "DarkBase",
205 | "SourceIndex": "0"
206 | },
207 | {
208 | "Target": "BaseMediumLow",
209 | "Source": "DarkBase",
210 | "SourceIndex": "3"
211 | },
212 | {
213 | "Target": "ChromeAltLow",
214 | "Source": "DarkBase",
215 | "SourceIndex": "0"
216 | },
217 | {
218 | "Target": "ChromeBlackHigh",
219 | "Source": "Black"
220 | },
221 | {
222 | "Target": "ChromeBlackLow",
223 | "Source": "DarkBase",
224 | "SourceIndex": "0"
225 | },
226 | {
227 | "Target": "ChromeBlackMedium",
228 | "Source": "Black"
229 | },
230 | {
231 | "Target": "ChromeBlackMediumLow",
232 | "Source": "Black"
233 | },
234 | {
235 | "Target": "ChromeDisabledHigh",
236 | "Source": "DarkBase",
237 | "SourceIndex": "5"
238 | },
239 | {
240 | "Target": "ChromeDisabledLow",
241 | "Source": "DarkBase",
242 | "SourceIndex": "1"
243 | },
244 | {
245 | "Target": "ChromeGray",
246 | "Source": "DarkBase",
247 | "SourceIndex": "2"
248 | },
249 | {
250 | "Target": "ChromeHigh",
251 | "Source": "DarkBase",
252 | "SourceIndex": "2"
253 | },
254 | {
255 | "Target": "ChromeLow",
256 | "Source": "DarkBase",
257 | "SourceIndex": "9"
258 | },
259 | {
260 | "Target": "ChromeMedium",
261 | "Source": "DarkBase",
262 | "SourceIndex": "8"
263 | },
264 | {
265 | "Target": "ChromeMediumLow",
266 | "Source": "DarkBase",
267 | "SourceIndex": "6"
268 | },
269 | {
270 | "Target": "ChromeWhite",
271 | "Source": "White"
272 | },
273 | {
274 | "Target": "ListLow",
275 | "Source": "DarkBase",
276 | "SourceIndex": "8"
277 | },
278 | {
279 | "Target": "ListMedium",
280 | "Source": "DarkBase",
281 | "SourceIndex": "5"
282 | }
283 | ],
284 | "Presets": [
285 | {
286 | "Id": "Default",
287 | "Name": "Default",
288 | "LightRegionColor": "#FFFFFFFF",
289 | "DarkRegionColor": "#FF000000",
290 | "LightBaseColor": "#FFCCCCCC",
291 | "DarkBaseColor": "#FF333333",
292 | "LightPrimaryColor": "#FF0073CF",
293 | "DarkPrimaryColor": "#FF0073CF"
294 | },
295 | {
296 | "Id": "Lavender",
297 | "Name": "Lavender",
298 | "LightRegionColor": "#FFFEF6FF",
299 | "DarkRegionColor": "#FF262738",
300 | "LightBaseColor": "#FFEECEFF",
301 | "DarkBaseColor": "#FF64576B",
302 | "LightPrimaryColor": "#FF8961CC",
303 | "DarkPrimaryColor": "#FF8961CC"
304 | },
305 | {
306 | "Id": "Forest",
307 | "Name": "Forest",
308 | "LightRegionColor": "#FFF7FFFF",
309 | "DarkRegionColor": "#FF353819",
310 | "LightBaseColor": "#FFC2DB65",
311 | "DarkBaseColor": "#FF784834",
312 | "LightPrimaryColor": "#FF34854D",
313 | "DarkPrimaryColor": "#FF34854D"
314 | },
315 | {
316 | "Id": "Nighttime",
317 | "Name": "Nighttime",
318 | "LightRegionColor": "#FFCFEAFF",
319 | "DarkRegionColor": "#FF0D2644",
320 | "LightBaseColor": "#FF7CBEE0",
321 | "DarkBaseColor": "#FF2F7BAD",
322 | "LightPrimaryColor": "#FFCC4D11",
323 | "DarkPrimaryColor": "#FFCC4D11"
324 | }
325 | ]
326 | }
--------------------------------------------------------------------------------
/FluentEditorShared/Resources/demodata.json:
--------------------------------------------------------------------------------
1 | {
2 | "Demos": [
3 | {
4 | "Type": "ControlPalette",
5 | "Id": "ControlPalette",
6 | "Title": "Control Palette Demo",
7 | "Glyph": ""
8 | }
9 | ],
10 | "DefaultDemoId": "ControlPalette"
11 | }
--------------------------------------------------------------------------------
/FluentEditorShared/StringProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Linq;
8 |
9 | namespace FluentEditorShared
10 | {
11 | public interface IStringProvider
12 | {
13 | string GetString(string id);
14 | }
15 |
16 | public class StringProvider : IStringProvider
17 | {
18 | private readonly Dictionary> _properties;
19 |
20 | [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Resources.Resources))]
21 | public StringProvider()
22 | {
23 | _properties = typeof(Resources.Resources).GetProperties()
24 | .ToDictionary(
25 | p => p.Name,
26 | p => (Func)p.GetMethod!.CreateDelegate(typeof(Func)));
27 | }
28 |
29 | public string GetString(string id)
30 | {
31 | return _properties.TryGetValue(id, out var func) ? func() : id;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/FluentEditorShared/Utils/ColorBlending.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia.Media;
6 |
7 | namespace FluentEditorShared.Utils
8 | {
9 | public enum ColorBlendMode { Burn, Darken, Dodge, Lighten, Multiply, Overlay, Screen }
10 |
11 | public static class ColorBlending
12 | {
13 | public const double DefaultSaturationConstant = 18.0;
14 | public const double DefaultDarkenConstant = 18.0;
15 |
16 | public static NormalizedRGB SaturateViaLCH(in NormalizedRGB input, double saturation, double saturationConstant = DefaultSaturationConstant)
17 | {
18 | LCH lch = ColorUtils.RGBToLCH(input, false);
19 | double saturated = lch.C + saturation * saturationConstant;
20 | if (saturated < 0)
21 | {
22 | saturated = 0;
23 | }
24 | return ColorUtils.LCHToRGB(new LCH(lch.L, saturated, lch.H, false), false);
25 | }
26 |
27 | public static NormalizedRGB DesaturateViaLCH(in NormalizedRGB input, double saturation, double saturationConstant = DefaultSaturationConstant)
28 | {
29 | return SaturateViaLCH(input, -1.0 * saturation, saturationConstant);
30 | }
31 |
32 | public static NormalizedRGB DarkenViaLAB(in NormalizedRGB input, double amount, double darkenConstant = DefaultDarkenConstant)
33 | {
34 | LAB lab = ColorUtils.RGBToLAB(input, false);
35 | double darkened = lab.L - amount * darkenConstant;
36 | return ColorUtils.LABToRGB(new LAB(darkened, lab.A, lab.B), false);
37 | }
38 |
39 | public static NormalizedRGB LightenViaLAB(in NormalizedRGB input, double amount, double darkenConstant = DefaultDarkenConstant)
40 | {
41 | return DarkenViaLAB(input, -1.0 * amount, darkenConstant);
42 | }
43 |
44 | public static NormalizedRGB Blend(in NormalizedRGB bottom, in NormalizedRGB top, ColorBlendMode mode)
45 | {
46 | switch (mode)
47 | {
48 | case ColorBlendMode.Burn:
49 | return BlendBurn(bottom, top);
50 | case ColorBlendMode.Darken:
51 | return BlendDarken(bottom, top);
52 | case ColorBlendMode.Dodge:
53 | return BlendDodge(bottom, top);
54 | case ColorBlendMode.Lighten:
55 | return BlendLighten(bottom, top);
56 | case ColorBlendMode.Multiply:
57 | return BlendMultiply(bottom, top);
58 | case ColorBlendMode.Overlay:
59 | return BlendOverlay(bottom, top);
60 | case ColorBlendMode.Screen:
61 | return BlendScreen(bottom, top);
62 | default:
63 | throw new ArgumentException("Unknown blend mode", "mode");
64 | }
65 | }
66 |
67 | public static NormalizedRGB BlendBurn(in NormalizedRGB bottom, in NormalizedRGB top)
68 | {
69 | return new NormalizedRGB(BlendBurn(bottom.R, top.R), BlendBurn(bottom.G, top.G), BlendBurn(bottom.B, top.B), false);
70 | }
71 |
72 | // single channel in the range [0.0,1.0]
73 | public static double BlendBurn(double bottom, double top)
74 | {
75 | if (top == 0.0)
76 | {
77 | // Despite the discontinuity, other sources seem to use 0.0 here instead of 1
78 | return 0.0;
79 | }
80 | return 1.0 - (1.0 - bottom) / top;
81 | }
82 |
83 | public static NormalizedRGB BlendDarken(in NormalizedRGB bottom, in NormalizedRGB top)
84 | {
85 | return new NormalizedRGB(BlendDarken(bottom.R, top.R), BlendDarken(bottom.G, top.G), BlendDarken(bottom.B, top.B), false);
86 | }
87 |
88 | // single channel in the range [0.0,1.0]
89 | public static double BlendDarken(double bottom, double top)
90 | {
91 | return Math.Min(bottom, top);
92 | }
93 |
94 | public static NormalizedRGB BlendDodge(in NormalizedRGB bottom, in NormalizedRGB top)
95 | {
96 | return new NormalizedRGB(BlendDodge(bottom.R, top.R), BlendDodge(bottom.G, top.G), BlendDodge(bottom.B, top.B), false);
97 | }
98 |
99 | // single channel in the range [0.0,1.0]
100 | public static double BlendDodge(double bottom, double top)
101 | {
102 | if (top >= 1.0)
103 | {
104 | return 1.0;
105 | }
106 | double retVal = bottom / (1.0 - top);
107 | if (retVal >= 1.0)
108 | {
109 | return 1.0;
110 | }
111 | return retVal;
112 | }
113 |
114 | public static NormalizedRGB BlendLighten(in NormalizedRGB bottom, in NormalizedRGB top)
115 | {
116 | return new NormalizedRGB(BlendLighten(bottom.R, top.R), BlendLighten(bottom.G, top.G), BlendLighten(bottom.B, top.B), false);
117 | }
118 |
119 | // single channel in the range [0.0,1.0]
120 | public static double BlendLighten(double bottom, double top)
121 | {
122 | return Math.Max(bottom, top);
123 | }
124 |
125 | public static NormalizedRGB BlendMultiply(in NormalizedRGB bottom, in NormalizedRGB top)
126 | {
127 | return new NormalizedRGB(BlendMultiply(bottom.R, top.R), BlendMultiply(bottom.G, top.G), BlendMultiply(bottom.B, top.B), false);
128 | }
129 |
130 | // single channel in the range [0.0,1.0]
131 | public static double BlendMultiply(double bottom, double top)
132 | {
133 | return bottom * top;
134 | }
135 |
136 | public static NormalizedRGB BlendOverlay(in NormalizedRGB bottom, in NormalizedRGB top)
137 | {
138 | return new NormalizedRGB(BlendOverlay(bottom.R, top.R), BlendOverlay(bottom.G, top.G), BlendOverlay(bottom.B, top.B), false);
139 | }
140 |
141 | // single channel in the range [0.0,1.0]
142 | public static double BlendOverlay(double bottom, double top)
143 | {
144 | if (bottom < 0.5)
145 | {
146 | return MathUtils.ClampToUnit(2.0 * top * bottom);
147 | }
148 |
149 | return MathUtils.ClampToUnit(1.0 - 2.0 * (1.0 - top) * (1.0 - bottom));
150 | }
151 |
152 | public static NormalizedRGB BlendScreen(in NormalizedRGB bottom, in NormalizedRGB top)
153 | {
154 | return new NormalizedRGB(BlendScreen(bottom.R, top.R), BlendScreen(bottom.G, top.G), BlendScreen(bottom.B, top.B), false);
155 | }
156 |
157 | // single channel in the range [0.0,1.0]
158 | public static double BlendScreen(double bottom, double top)
159 | {
160 | return 1.0 - (1.0 - top) * (1.0 - bottom);
161 | }
162 |
163 |
164 | // Alpha channel of background is ignored
165 | // The returned color always has an alpha channel of 0xff
166 | // Different programs (eg: paint.net, photoshop) will give different answers than this occasionally but within +/- 1 in each channel
167 | // just depends on the details of how they round off decimals
168 | public static Color ComputeAlphaBlend(Color foreground, Color background)
169 | {
170 | if (foreground.A == 255)
171 | {
172 | return foreground;
173 | }
174 | if (foreground.A == 0)
175 | {
176 | return Color.FromArgb(255, background.R, background.G, background.B);
177 | }
178 |
179 | double fr = foreground.R / 255.0;
180 | double fg = foreground.G / 255.0;
181 | double fb = foreground.B / 255.0;
182 | double fa = foreground.A / 255.0;
183 |
184 | double br = background.R / 255.0;
185 | double bg = background.G / 255.0;
186 | double bb = background.B / 255.0;
187 |
188 | double finalr = fa * fr + (1.0 - fa) * br;
189 | double finalg = fa * fg + (1.0 - fa) * bg;
190 | double finalb = fa * fb + (1.0 - fa) * bb;
191 |
192 | return Color.FromArgb(255, (byte)Math.Round(255.0 * finalr), (byte)Math.Round(255.0 * finalg), (byte)Math.Round(255.0 * finalb));
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/FluentEditorShared/Utils/ColorScale.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using Avalonia.Media;
8 |
9 | namespace FluentEditorShared.Utils
10 | {
11 | public enum ColorScaleInterpolationMode { RGB, LAB, XYZ }
12 |
13 | public readonly struct ColorScaleStop
14 | {
15 | public ColorScaleStop(Color color, double position)
16 | {
17 | Color = color;
18 | Position = position;
19 | }
20 |
21 | public ColorScaleStop(ColorScaleStop source)
22 | {
23 | Color = source.Color;
24 | Position = source.Position;
25 | }
26 |
27 | public readonly Color Color;
28 | public readonly double Position;
29 | }
30 |
31 | public class ColorScale
32 | {
33 | // Evenly distributes the colors provided between 0 and 1
34 | public ColorScale(IEnumerable colors)
35 | {
36 | if (colors == null)
37 | {
38 | throw new ArgumentNullException("colors");
39 | }
40 |
41 | int count = colors.Count();
42 | _stops = new ColorScaleStop[count];
43 | int index = 0;
44 | foreach (Color color in colors)
45 | {
46 | // Clean up floating point jaggies
47 | if (index == 0)
48 | {
49 | _stops[index] = new ColorScaleStop(color, 0);
50 | }
51 | else if (index == count - 1)
52 | {
53 | _stops[index] = new ColorScaleStop(color, 1);
54 | }
55 | else
56 | {
57 | _stops[index] = new ColorScaleStop(color, index * (1.0 / (count - 1)));
58 | }
59 | index++;
60 | }
61 | }
62 |
63 | public ColorScale(IEnumerable stops)
64 | {
65 | if (stops == null)
66 | {
67 | throw new ArgumentNullException("stops");
68 | }
69 |
70 | int count = stops.Count();
71 | _stops = new ColorScaleStop[count];
72 | int index = 0;
73 | foreach (ColorScaleStop stop in stops)
74 | {
75 | _stops[index] = new ColorScaleStop(stop);
76 | index++;
77 | }
78 | }
79 |
80 | public ColorScale(ColorScale source)
81 | {
82 | if (source == null)
83 | {
84 | throw new ArgumentNullException("source");
85 | }
86 |
87 | _stops = new ColorScaleStop[source._stops.Length];
88 | for (int i = 0; i < _stops.Length; i++)
89 | {
90 | _stops[i] = new ColorScaleStop(source._stops[i]);
91 | }
92 | }
93 |
94 | private readonly ColorScaleStop[] _stops;
95 |
96 | public Color GetColor(double position, ColorScaleInterpolationMode mode = ColorScaleInterpolationMode.RGB)
97 | {
98 | if (_stops.Length == 1)
99 | {
100 | return _stops[0].Color;
101 | }
102 | if (position <= 0)
103 | {
104 | return _stops[0].Color;
105 | }
106 |
107 | if (position >= 1)
108 | {
109 | return _stops[_stops.Length - 1].Color;
110 | }
111 | int lowerIndex = 0;
112 | for (int i = 0; i < _stops.Length; i++)
113 | {
114 | if (_stops[i].Position <= position)
115 | {
116 | lowerIndex = i;
117 | }
118 | }
119 | int upperIndex = lowerIndex + 1;
120 | if (upperIndex >= _stops.Length)
121 | {
122 | upperIndex = _stops.Length - 1;
123 | }
124 | double scalePosition = (position - _stops[lowerIndex].Position) * (1.0 / (_stops[upperIndex].Position - _stops[lowerIndex].Position));
125 |
126 | switch (mode)
127 | {
128 | case ColorScaleInterpolationMode.LAB:
129 | LAB leftLAB = ColorUtils.RGBToLAB(_stops[lowerIndex].Color, false);
130 | LAB rightLAB = ColorUtils.RGBToLAB(_stops[upperIndex].Color, false);
131 | LAB targetLAB = ColorUtils.InterpolateLAB(leftLAB, rightLAB, scalePosition);
132 | return ColorUtils.LABToRGB(targetLAB, false).Denormalize();
133 | case ColorScaleInterpolationMode.XYZ:
134 | XYZ leftXYZ = ColorUtils.RGBToXYZ(_stops[lowerIndex].Color, false);
135 | XYZ rightXYZ = ColorUtils.RGBToXYZ(_stops[upperIndex].Color, false);
136 | XYZ targetXYZ = ColorUtils.InterpolateXYZ(leftXYZ, rightXYZ, scalePosition);
137 | return ColorUtils.XYZToRGB(targetXYZ, false).Denormalize();
138 | default:
139 | return ColorUtils.InterpolateRGB(_stops[lowerIndex].Color, _stops[upperIndex].Color, scalePosition);
140 | }
141 | }
142 |
143 | public ColorScale Trim(double lowerBound, double upperBound, ColorScaleInterpolationMode mode = ColorScaleInterpolationMode.RGB)
144 | {
145 | if (lowerBound < 0 || upperBound > 1 || upperBound < lowerBound)
146 | {
147 | throw new ArgumentException("Invalid bounds");
148 | }
149 | if (lowerBound == upperBound)
150 | {
151 | return new ColorScale(new[] { GetColor(lowerBound, mode) });
152 | }
153 | List containedStops = new List(_stops.Length);
154 |
155 | for (int i = 0; i < _stops.Length; i++)
156 | {
157 | if (_stops[i].Position >= lowerBound && _stops[i].Position <= upperBound)
158 | {
159 | containedStops.Add(_stops[i]);
160 | }
161 | }
162 |
163 | if (containedStops.Count == 0)
164 | {
165 | return new ColorScale(new[] { GetColor(lowerBound, mode), GetColor(upperBound, mode) });
166 | }
167 |
168 | if (containedStops.First().Position != lowerBound)
169 | {
170 | containedStops.Insert(0, new ColorScaleStop(GetColor(lowerBound, mode), lowerBound));
171 | }
172 | if (containedStops.Last().Position != upperBound)
173 | {
174 | containedStops.Add(new ColorScaleStop(GetColor(upperBound, mode), upperBound));
175 | }
176 |
177 | double range = upperBound - lowerBound;
178 | ColorScaleStop[] finalStops = new ColorScaleStop[containedStops.Count];
179 | for (int i = 0; i < finalStops.Length; i++)
180 | {
181 | double adjustedPosition = (containedStops[i].Position - lowerBound) / range;
182 | finalStops[i] = new ColorScaleStop(containedStops[i].Color, adjustedPosition);
183 | }
184 | return new ColorScale(finalStops);
185 | }
186 |
187 | public double FindNextColor(double position, double contrast, bool searchDown = false, ColorScaleInterpolationMode mode = ColorScaleInterpolationMode.RGB, double contrastErrorMargin = 0.005, int maxSearchIterations = 32)
188 | {
189 | if (position >= 1)
190 | {
191 | return 1;
192 | }
193 | if (position < 0)
194 | {
195 | position = 0;
196 | }
197 | Color startingColor = GetColor(position, mode);
198 | double finalPosition = 0.0;
199 | if (!searchDown)
200 | {
201 | finalPosition = 1.0;
202 | }
203 | Color finalColor = GetColor(finalPosition, mode);
204 | double finalContrast = ColorUtils.ContrastRatio(startingColor, finalColor, false);
205 | if (finalContrast <= contrast)
206 | {
207 | return finalPosition;
208 | }
209 |
210 | double testRangeMin, testRangeMax;
211 | if (searchDown)
212 | {
213 | testRangeMin = 0.0;
214 | testRangeMax = position;
215 | }
216 | else
217 | {
218 | testRangeMin = position;
219 | testRangeMax = 1.0;
220 | }
221 | double mid = finalPosition;
222 | int iterations = 0;
223 | while (iterations <= maxSearchIterations)
224 | {
225 | mid = Math.Abs(testRangeMax - testRangeMin) / 2.0 + testRangeMin;
226 | Color midColor = GetColor(mid, mode);
227 | double midContrast = ColorUtils.ContrastRatio(startingColor, midColor);
228 |
229 | if (Math.Abs(midContrast - contrast) <= contrastErrorMargin)
230 | {
231 | return mid;
232 | }
233 |
234 | if (midContrast > contrast)
235 | {
236 | if(searchDown)
237 | {
238 | testRangeMin = mid;
239 | }
240 | else
241 | {
242 | testRangeMax = mid;
243 | }
244 | }
245 | else
246 | {
247 | if (searchDown)
248 | {
249 | testRangeMax = mid;
250 | }
251 | else
252 | {
253 | testRangeMin = mid;
254 | }
255 | }
256 |
257 | iterations++;
258 | }
259 |
260 | return mid;
261 | }
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/FluentEditorShared/Utils/ColorTypes.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia.Media;
6 |
7 | namespace FluentEditorShared.Utils
8 | {
9 | public enum ColorStringFormat { RGB, PoundRGB, ARGB, NormalizedRGB, HSL, HSV, LAB, LCH, XYZ, Luminance, Temperature }
10 |
11 | // Valid values for each channel are ∈ [0.0,1.0]
12 | // But sometimes it is useful to allow values outside that range during calculations as long as they are clamped eventually
13 | public readonly struct NormalizedRGB : IEquatable
14 | {
15 | public const int DefaultRoundingPrecision = 5;
16 |
17 | public NormalizedRGB(double r, double g, double b, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
18 | {
19 | if (round)
20 | {
21 | R = Math.Round(r, roundingPrecision);
22 | G = Math.Round(g, roundingPrecision);
23 | B = Math.Round(b, roundingPrecision);
24 | }
25 | else
26 | {
27 | R = r;
28 | G = g;
29 | B = b;
30 | }
31 | }
32 |
33 | public NormalizedRGB(in Color rgb, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
34 | {
35 | if (round)
36 | {
37 | R = Math.Round(rgb.R / 255.0, roundingPrecision);
38 | G = Math.Round(rgb.G / 255.0, roundingPrecision);
39 | B = Math.Round(rgb.B / 255.0, roundingPrecision);
40 | }
41 | else
42 | {
43 | R = rgb.R / 255.0;
44 | G = rgb.G / 255.0;
45 | B = rgb.B / 255.0;
46 | }
47 | }
48 |
49 | public Color Denormalize(byte a = 255)
50 | {
51 | return Color.FromArgb(a, MathUtils.ClampToByte(R * 255.0), MathUtils.ClampToByte(G * 255.0), MathUtils.ClampToByte(B * 255.0));
52 | }
53 |
54 | public readonly double R;
55 | public readonly double G;
56 | public readonly double B;
57 |
58 | #region IEquatable
59 |
60 | public bool Equals(NormalizedRGB other)
61 | {
62 | return R == other.R && G == other.G && B == other.B;
63 | }
64 |
65 | #endregion
66 |
67 | #region Equals
68 |
69 | public override bool Equals(object obj)
70 | {
71 | if (obj is NormalizedRGB other)
72 | {
73 | return R == other.R && G == other.G && B == other.B;
74 | }
75 |
76 | return base.Equals(obj);
77 | }
78 |
79 | public override int GetHashCode()
80 | {
81 | return R.GetHashCode() ^ G.GetHashCode() ^ B.GetHashCode();
82 | }
83 |
84 | #endregion
85 |
86 | #region ToString
87 |
88 | public override string ToString()
89 | {
90 | return string.Format("{0},{1},{2}", R, G, B);
91 | }
92 |
93 | #endregion
94 | }
95 |
96 | // H ∈ [0.0,360.0]
97 | // S ∈ [0.0,1.0]
98 | // L ∈ [0.0,1.0]
99 | public readonly struct HSL : IEquatable
100 | {
101 | public const int DefaultRoundingPrecision = 5;
102 |
103 | public HSL(double h, double s, double l, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
104 | {
105 | if (round)
106 | {
107 | H = Math.Round(h, roundingPrecision);
108 | S = Math.Round(s, roundingPrecision);
109 | L = Math.Round(l, roundingPrecision);
110 | }
111 | else
112 | {
113 | H = h;
114 | S = s;
115 | L = l;
116 | }
117 | }
118 |
119 | public readonly double H;
120 | public readonly double S;
121 | public readonly double L;
122 |
123 | #region IEquatable
124 |
125 | public bool Equals(HSL other)
126 | {
127 | return H == other.H && S == other.S && L == other.L;
128 | }
129 |
130 | #endregion
131 |
132 | #region Equals
133 |
134 | public override bool Equals(object obj)
135 | {
136 | if (obj is HSL other)
137 | {
138 | return H == other.H && S == other.S && L == other.L;
139 | }
140 |
141 | return base.Equals(obj);
142 | }
143 |
144 | public override int GetHashCode()
145 | {
146 | return H.GetHashCode() ^ S.GetHashCode() ^ L.GetHashCode();
147 | }
148 |
149 | #endregion
150 |
151 | #region ToString
152 |
153 | public override string ToString()
154 | {
155 | return string.Format("{0},{1},{2}", H, S, L);
156 | }
157 |
158 | #endregion
159 | }
160 |
161 | // H ∈ [0.0,360.0]
162 | // S ∈ [0.0,1.0]
163 | // V ∈ [0.0,1.0]
164 | public readonly struct HSV : IEquatable
165 | {
166 | public const int DefaultRoundingPrecision = 5;
167 |
168 | public HSV(double h, double s, double v, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
169 | {
170 | if (round)
171 | {
172 | H = Math.Round(h, roundingPrecision);
173 | S = Math.Round(s, roundingPrecision);
174 | V = Math.Round(v, roundingPrecision);
175 | }
176 | else
177 | {
178 | H = h;
179 | S = s;
180 | V = v;
181 | }
182 | }
183 |
184 | public readonly double H;
185 | public readonly double S;
186 | public readonly double V;
187 |
188 | #region IEquatable
189 |
190 | public bool Equals(HSV other)
191 | {
192 | return H == other.H && S == other.S && V == other.V;
193 | }
194 |
195 | #endregion
196 |
197 | #region Equals
198 |
199 | public override bool Equals(object obj)
200 | {
201 | if (obj is HSV other)
202 | {
203 | return H == other.H && S == other.S && V == other.V;
204 | }
205 |
206 | return base.Equals(obj);
207 | }
208 |
209 | public override int GetHashCode()
210 | {
211 | return H.GetHashCode() ^ S.GetHashCode() ^ V.GetHashCode();
212 | }
213 |
214 | #endregion
215 |
216 | #region ToString
217 |
218 | public override string ToString()
219 | {
220 | return string.Format("{0},{1},{2}", H, S, V);
221 | }
222 |
223 | #endregion
224 | }
225 |
226 | public readonly struct LAB : IEquatable
227 | {
228 | public const int DefaultRoundingPrecision = 5;
229 |
230 | public LAB(double l, double a, double b, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
231 | {
232 | if (round)
233 | {
234 | L = Math.Round(l, roundingPrecision);
235 | A = Math.Round(a, roundingPrecision);
236 | B = Math.Round(b, roundingPrecision);
237 | }
238 | else
239 | {
240 | L = l;
241 | A = a;
242 | B = b;
243 | }
244 | }
245 |
246 | public readonly double L;
247 | public readonly double A;
248 | public readonly double B;
249 |
250 | #region IEquatable
251 |
252 | public bool Equals(LAB other)
253 | {
254 | return L == other.L && A == other.A && B == other.B;
255 | }
256 |
257 | #endregion
258 |
259 | #region Equals
260 |
261 | public override bool Equals(object obj)
262 | {
263 | if (obj is LAB other)
264 | {
265 | return L == other.L && A == other.A && B == other.B;
266 | }
267 |
268 | return base.Equals(obj);
269 | }
270 |
271 | public override int GetHashCode()
272 | {
273 | return L.GetHashCode() ^ A.GetHashCode() ^ B.GetHashCode();
274 | }
275 |
276 | #endregion
277 |
278 | #region ToString
279 |
280 | public override string ToString()
281 | {
282 | return string.Format("{0},{1},{2}", L, A, B);
283 | }
284 |
285 | #endregion
286 | }
287 |
288 | public readonly struct LCH : IEquatable
289 | {
290 | public const int DefaultRoundingPrecision = 5;
291 |
292 | public LCH(double l, double c, double h, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
293 | {
294 | if (round)
295 | {
296 | L = Math.Round(l, roundingPrecision);
297 | C = Math.Round(c, roundingPrecision);
298 | H = Math.Round(h, roundingPrecision);
299 | }
300 | else
301 | {
302 | L = l;
303 | C = c;
304 | H = h;
305 | }
306 | }
307 |
308 | public readonly double L;
309 | public readonly double C;
310 | public readonly double H;
311 |
312 | #region IEquatable
313 |
314 | public bool Equals(LCH other)
315 | {
316 | return L == other.L && C == other.C && H == other.H;
317 | }
318 |
319 | #endregion
320 |
321 | #region Equals
322 |
323 | public override bool Equals(object obj)
324 | {
325 | if (obj is LCH other)
326 | {
327 | return L == other.L && C == other.C && H == other.H;
328 | }
329 |
330 | return base.Equals(obj);
331 | }
332 |
333 | public override int GetHashCode()
334 | {
335 | return L.GetHashCode() ^ C.GetHashCode() ^ H.GetHashCode();
336 | }
337 |
338 | #endregion
339 |
340 | #region ToString
341 |
342 | public override string ToString()
343 | {
344 | return string.Format("{0},{1},{2}", L, C, H);
345 | }
346 |
347 | #endregion
348 | }
349 |
350 | public readonly struct XYZ : IEquatable
351 | {
352 | public const int DefaultRoundingPrecision = 5;
353 |
354 | public XYZ(double x, double y, double z, bool round = true, int roundingPrecision = DefaultRoundingPrecision)
355 | {
356 | if (round)
357 | {
358 | X = Math.Round(x, roundingPrecision);
359 | Y = Math.Round(y, roundingPrecision);
360 | Z = Math.Round(z, roundingPrecision);
361 | }
362 | else
363 | {
364 | X = x;
365 | Y = y;
366 | Z = z;
367 | }
368 | }
369 |
370 | public readonly double X;
371 | public readonly double Y;
372 | public readonly double Z;
373 |
374 | #region IEquatable
375 |
376 | public bool Equals(XYZ other)
377 | {
378 | return X == other.X && Y == other.Y && Z == other.Z;
379 | }
380 |
381 | #endregion
382 |
383 | #region Equals
384 |
385 | public override bool Equals(object obj)
386 | {
387 | if (obj is XYZ other)
388 | {
389 | return X == other.X && Y == other.Y && Z == other.Z;
390 | }
391 |
392 | return base.Equals(obj);
393 | }
394 |
395 | public override int GetHashCode()
396 | {
397 | return X.GetHashCode() ^ Y.GetHashCode() ^ Z.GetHashCode();
398 | }
399 |
400 | #endregion
401 |
402 | #region ToString
403 |
404 | public override string ToString()
405 | {
406 | return string.Format("{0},{1},{2}", X, Y, Z);
407 | }
408 |
409 | #endregion
410 | }
411 | }
412 |
--------------------------------------------------------------------------------
/FluentEditorShared/Utils/JSONExtensionMethods.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using System.Globalization;
6 | using System.Numerics;
7 | using System.Text.Json.Nodes;
8 | using Avalonia.Media;
9 |
10 | namespace FluentEditorShared.Utils
11 | {
12 | public static class JsonExtensionMethods
13 | {
14 | public static string GetOptionalString(this JsonObject data, string key)
15 | {
16 | if (data == null || string.IsNullOrEmpty(key))
17 | {
18 | return null;
19 | }
20 | if (data.ContainsKey(key))
21 | {
22 | return data[key].GetOptionalString();
23 | }
24 |
25 | return null;
26 | }
27 |
28 | public static string GetOptionalString(this JsonNode data)
29 | {
30 | if (data == null)
31 | {
32 | return null;
33 | }
34 | if (data is JsonValue value
35 | && value.TryGetValue(out var val))
36 | {
37 | return val;
38 | }
39 | return data.ToString();
40 | }
41 |
42 | public static int GetInt(this JsonNode data)
43 | {
44 | if (data == null)
45 | {
46 | throw new ArgumentNullException("data");
47 | }
48 | if (data is JsonValue value)
49 | {
50 | if (value.TryGetValue(out var val))
51 | {
52 | int retVal;
53 | if (!int.TryParse(val, out retVal))
54 | {
55 | throw new Exception("JsonObject is type string but cannot be parsed to int");
56 | }
57 |
58 | return retVal;
59 | }
60 |
61 | if (value.TryGetValue(out var number))
62 | {
63 | return number;
64 | }
65 | }
66 |
67 | throw new InvalidOperationException();
68 | }
69 |
70 | public static bool TryGetInt(this JsonNode data, out int retVal)
71 | {
72 | if (data == null)
73 | {
74 | retVal = default(int);
75 | return false;
76 | }
77 | if (data is JsonValue value)
78 | {
79 | if (value.TryGetValue(out var val))
80 | {
81 | if (!int.TryParse(val, out retVal))
82 | {
83 | throw new Exception("JsonObject is type string but cannot be parsed to int");
84 | }
85 |
86 | return true;
87 | }
88 |
89 | if (value.TryGetValue(out retVal))
90 | {
91 | return true;
92 | }
93 | }
94 |
95 | retVal = default;
96 | return false;
97 | }
98 |
99 | public static bool TryGetFloat(this JsonNode data, out float retVal)
100 | {
101 | if (data == null)
102 | {
103 | retVal = default(float);
104 | return false;
105 | }
106 | if (data is JsonValue value)
107 | {
108 | if (value.TryGetValue(out var val))
109 | {
110 | if (!float.TryParse(val, NumberStyles.Any, CultureInfo.InvariantCulture, out retVal))
111 | {
112 | throw new Exception("JsonObject is type string but cannot be parsed to int");
113 | }
114 |
115 | return true;
116 | }
117 |
118 | if (value.TryGetValue(out retVal))
119 | {
120 | return true;
121 | }
122 | }
123 | retVal = default(float);
124 | return false;
125 | }
126 |
127 | public static Vector2 GetVector2(this JsonObject data)
128 | {
129 | if (data == null)
130 | {
131 | return default(Vector2);
132 | }
133 | float x = 0f, y = 0f;
134 |
135 | JsonValue xnode = null;
136 | if (data.ContainsKey("X"))
137 | {
138 | xnode = data["X"].AsValue();
139 | }
140 | else if (data.ContainsKey("x"))
141 | {
142 | xnode = data["x"].AsValue();
143 | }
144 | if (xnode != null)
145 | {
146 | if (!xnode.TryGetFloat(out x))
147 | {
148 | x = 0f;
149 | }
150 | }
151 |
152 | JsonValue ynode = null;
153 | if (data.ContainsKey("Y"))
154 | {
155 | ynode = data["Y"].AsValue();
156 | }
157 | else if (data.ContainsKey("y"))
158 | {
159 | ynode = data["y"].AsValue();
160 | }
161 | if (ynode != null)
162 | {
163 | if (!ynode.TryGetFloat(out y))
164 | {
165 | y = 0f;
166 | }
167 | }
168 |
169 | return new Vector2(x, y);
170 | }
171 |
172 | public static Vector3 GetVector3(this JsonObject data)
173 | {
174 | if (data == null)
175 | {
176 | return default(Vector3);
177 | }
178 | float x = 0f, y = 0f, z = 0f;
179 |
180 | JsonValue xnode = null;
181 | if (data.ContainsKey("X"))
182 | {
183 | xnode = data["X"].AsValue();
184 | }
185 | else if (data.ContainsKey("x"))
186 | {
187 | xnode = data["x"].AsValue();
188 | }
189 | if (xnode != null)
190 | {
191 | if (!xnode.TryGetFloat(out x))
192 | {
193 | x = 0f;
194 | }
195 | }
196 |
197 | JsonValue ynode = null;
198 | if (data.ContainsKey("Y"))
199 | {
200 | ynode = data["Y"].AsValue();
201 | }
202 | else if (data.ContainsKey("y"))
203 | {
204 | ynode = data["y"].AsValue();
205 | }
206 | if (ynode != null)
207 | {
208 | if (!ynode.TryGetFloat(out y))
209 | {
210 | y = 0f;
211 | }
212 | }
213 |
214 | JsonValue znode = null;
215 | if (data.ContainsKey("Z"))
216 | {
217 | znode = data["Z"].AsValue();
218 | }
219 | else if (data.ContainsKey("z"))
220 | {
221 | znode = data["z"].AsValue();
222 | }
223 | if (znode != null)
224 | {
225 | if (!znode.TryGetFloat(out z))
226 | {
227 | z = 0f;
228 | }
229 | }
230 |
231 | return new Vector3(x, y, z);
232 | }
233 |
234 | public static Color GetColor(this JsonNode data)
235 | {
236 | if (data == null)
237 | {
238 | throw new ArgumentNullException("data");
239 | }
240 | if (data is JsonValue value
241 | && value.TryGetValue(out var str))
242 | {
243 | Color retVal;
244 | if (!ColorUtils.TryParseColorString(str, out retVal))
245 | {
246 | throw new Exception("JsonObject is type string but cannot be parsed to Color");
247 | }
248 | return retVal;
249 | }
250 |
251 | throw new InvalidOperationException();
252 | }
253 |
254 | public static T GetEnum(this JsonNode data) where T : struct
255 | {
256 | string dataString = data.GetOptionalString();
257 | if (Enum.TryParse(dataString, out T retVal))
258 | {
259 | return retVal;
260 | }
261 |
262 | throw new Exception(string.Format("Unable to parse {0} into enum of type {1}", dataString, typeof(T)));
263 | }
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/FluentEditorShared/Utils/MathUtils.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft Corporation. All rights reserved.
2 | // Licensed under the MIT License.
3 |
4 | using System;
5 | using Avalonia;
6 |
7 | namespace FluentEditorShared.Utils
8 | {
9 | public static class MathUtils
10 | {
11 | public static byte ClampToByte(double c)
12 | {
13 | if (double.IsNaN(c))
14 | {
15 | return 0;
16 | }
17 |
18 | if (double.IsPositiveInfinity(c))
19 | {
20 | return 255;
21 | }
22 |
23 | if (double.IsNegativeInfinity(c))
24 | {
25 | return 0;
26 | }
27 | c = Math.Round(c);
28 | if (c <= 0)
29 | {
30 | return 0;
31 | }
32 |
33 | if (c >= 255)
34 | {
35 | return 255;
36 | }
37 |
38 | return (byte)c;
39 | }
40 |
41 | public static double ClampToUnit(double c)
42 | {
43 | if (double.IsNaN(c))
44 | {
45 | return 0;
46 | }
47 |
48 | if (double.IsPositiveInfinity(c))
49 | {
50 | return 1;
51 | }
52 |
53 | if (double.IsNegativeInfinity(c))
54 | {
55 | return 0;
56 | }
57 | if (c <= 0)
58 | {
59 | return 0;
60 | }
61 |
62 | if (c >= 1)
63 | {
64 | return 1;
65 | }
66 |
67 | return c;
68 | }
69 |
70 | public static double DegreesToRadians(double degrees)
71 | {
72 | return degrees * (Math.PI / 180.0);
73 | }
74 |
75 | public static double RadiansToDegrees(double radians)
76 | {
77 | return radians * (180.0 / Math.PI);
78 | }
79 |
80 | public static double Lerp(double left, double right, double scale)
81 | {
82 | if (scale <= 0)
83 | {
84 | return left;
85 | }
86 |
87 | if (scale >= 1)
88 | {
89 | return right;
90 | }
91 | return left + scale * (right - left);
92 | }
93 |
94 | public static byte Lerp(byte left, byte right, double scale)
95 | {
96 | if (scale <= 0)
97 | {
98 | return left;
99 | }
100 |
101 | if (scale >= 1)
102 | {
103 | return right;
104 | }
105 |
106 | if (left == right)
107 | {
108 | return left;
109 | }
110 | double l = left;
111 | double r = right;
112 | return (byte)Math.Round(l + scale * (r - l));
113 | }
114 |
115 | public static double LerpAnglesInDegrees(double left, double right, double scale)
116 | {
117 | if (scale <= 0)
118 | {
119 | return left;
120 | }
121 |
122 | if (scale >= 1)
123 | {
124 | return right;
125 | }
126 | var a = (left - right + 360.0) % 360.0;
127 | var b = (right - left + 360.0) % 360.0;
128 | if (a <= b)
129 | {
130 | return (left - a * scale + 360.0) % 360.0;
131 | }
132 |
133 | return (left + a * scale + 360.0) % 360.0;
134 | }
135 |
136 | public static double Interpolate(double start, double end, double progress, Func easing = null)
137 | {
138 | if (progress <= 0)
139 | {
140 | return start;
141 | }
142 |
143 | if (progress >= 1)
144 | {
145 | return end;
146 | }
147 | if (easing != null)
148 | {
149 | progress = easing(progress);
150 | }
151 | return start + (end - start) * progress;
152 | }
153 |
154 | public static Size Interpolate(in Size start, in Size end, double progress, Func easing = null)
155 | {
156 | if (progress <= 0)
157 | {
158 | return start;
159 | }
160 |
161 | if (progress >= 1)
162 | {
163 | return end;
164 | }
165 | if (easing != null)
166 | {
167 | progress = easing(progress);
168 | }
169 | return new Size(start.Width + (end.Width - start.Width) * progress, start.Height + (end.Height - start.Height) * progress);
170 | }
171 |
172 | public static Rect Interpolate(in Rect start, in Rect end, double progress, Func easing = null)
173 | {
174 | if (progress <= 0)
175 | {
176 | return start;
177 | }
178 |
179 | if (progress >= 1)
180 | {
181 | return end;
182 | }
183 | if (easing != null)
184 | {
185 | progress = easing(progress);
186 | }
187 | return new Rect(start.X + (end.X - start.X) * progress,
188 | start.Y + (end.Y - start.Y) * progress,
189 | start.Width + (end.Width - start.Width) * progress,
190 | start.Height + (end.Height - start.Height) * progress);
191 | }
192 |
193 | public static double LinearEasing(double input)
194 | {
195 | return input;
196 | }
197 |
198 | public static double QuadraticEasing(double input)
199 | {
200 | return input * input;
201 | }
202 |
203 | public static double CubicEasing(double input)
204 | {
205 | return input * input * input;
206 | }
207 |
208 | public static double CubicBezierEasing(double x, in Point control1, in Point control2, int iterations = 4)
209 | {
210 | if (control1.X == control1.Y && control2.X == control2.Y)
211 | {
212 | return x;
213 | }
214 | double t = CubicBezierApproximateT(x, control1.X, control2.X, iterations);
215 | return CubicBezier(t, control1, control2).Y;
216 | }
217 |
218 | // Uses Newton Raphson method
219 | public static double CubicBezierApproximateT(double x, double control1, double control2, int iterations = 4)
220 | {
221 | if (x <= 0.0)
222 | {
223 | return 0.0;
224 | }
225 | if (x >= 1.0)
226 | {
227 | return 1.0;
228 | }
229 |
230 | double t = x;
231 | for (int i = 0; i < iterations; i++)
232 | {
233 | double slope = CubicBezierFirstDerivative(t, control1, control2);
234 | if (slope == 0.0)
235 | {
236 | return t;
237 | }
238 | double xp = CubicBezier(t, control1, control2) - x;
239 | t -= xp / slope;
240 | }
241 | return t;
242 | }
243 |
244 | public static Point CubicBezier(double t, in Point start, in Point control1, in Point control2, in Point end)
245 | {
246 | return new Point(CubicBezier(t, start.X, control1.X, control2.X, end.X), CubicBezier(t, start.Y, control1.Y, control2.Y, end.Y));
247 | }
248 |
249 | // Special case used in easing functions where start = 0,0 and end = 1,1
250 | public static Point CubicBezier(double t, in Point control1, in Point control2)
251 | {
252 | return new Point(CubicBezier(t, control1.X, control2.X), CubicBezier(t, control1.Y, control2.Y));
253 | }
254 |
255 | public static double CubicBezier(double t, double start, double control1, double control2, double end)
256 | {
257 | return start * Math.Pow(1.0 - t, 3) + control1 * 3.0 * t * Math.Pow(1.0 - t, 2) + control2 * 3.0 * t * t * (1.0 - t) + t * t * t * end;
258 | }
259 |
260 | // Special case used in easing functions where start = 0 and end = 1
261 | public static double CubicBezier(double t, double control1, double control2)
262 | {
263 | return control1 * 3.0 * t * Math.Pow(1.0 - t, 2) + control2 * 3.0 * t * t * (1.0 - t) + t * t * t;
264 | }
265 |
266 | public static double CubicBezierFirstDerivative(double t, double start, double control1, double control2, double end)
267 | {
268 | return 3.0 * Math.Pow(1.0 - t, 2) * (control1 - start) + 6.0 * t * (1.0 - t) * (control2 - control1) + 3.0 * t * t * (end - control2);
269 | }
270 |
271 | // Special case used in easing functions where start = 0 and end = 1
272 | public static double CubicBezierFirstDerivative(double t, double control1, double control2)
273 | {
274 | return 3.0 * Math.Pow(1.0 - t, 2) * (control1) + 6.0 * t * (1.0 - t) * (control2 - control1) + 3.0 * t * t * (1.0 - control2);
275 | }
276 |
277 | public static double CubicBezierSecondDerivative(double t, double start, double control1, double control2, double end)
278 | {
279 | return 6.0 * (1 - t) * (control2 - 2.0 * control1 + start) + 6.0 * t * (end - 2.0 * control2 + control1);
280 | }
281 |
282 | // Special case used in easing functions where start = 0 and end = 1
283 | public static double CubicBezierSecondDerivative(double t, double control1, double control2)
284 | {
285 | return 6.0 * (1 - t) * (control2 - 2.0 * control1) + 6.0 * t * (1.0 - 2.0 * control2 + control1);
286 | }
287 |
288 | public static double ComputeMean(double[] values, double[] weights = null)
289 | {
290 | if (values == null || values.Length == 0)
291 | {
292 | throw new ArgumentNullException("values");
293 | }
294 | if (weights != null && weights.Length != values.Length)
295 | {
296 | throw new Exception("The weights array must be either null or the same length as values");
297 | }
298 | double totalWeight = 0.0;
299 | double total = 0.0;
300 | for (int i = 0; i < values.Length; i++)
301 | {
302 | if (weights == null)
303 | {
304 | total += values[i];
305 | totalWeight++;
306 | }
307 | else
308 | {
309 | total += values[i] * weights[i];
310 | totalWeight += weights[i];
311 | }
312 | }
313 | if (totalWeight == 0.0)
314 | {
315 | return 0.0;
316 | }
317 | return total / totalWeight;
318 | }
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Fluent XAML Theme Editor
2 | ===
3 |
4 | This repo contains the full solution and source code to the Fluent XAML Theme Editor - a tool that helps demonstrate the flexibility of the [Fluent Design System](https://www.microsoft.com/design/fluent/) as well as supports the app development process by generating XAML markup for our ResourceDictionary framework used in Avalonia applications.
5 |
6 | It is a fork of [UWP Fluent XAML Theme Editor](https://github.com/microsoft/fluent-xaml-theme-editor/tree/97321946cf1f03128c98251f03da62b7334671af) by Microsoft, modified to work with Avalonia.
7 |
8 | Browser version is accessible from https://theme.xaml.live/
9 |
10 | 
11 |
12 | How to use the tool
13 | ---
14 | With the preview build, you can select three major colors for both the Light and Dark themes in the right-hand properties view labeled “Color Dictionary”.
15 |
16 | 
17 |
18 | - **Region** – The background that all the controls sit on, which is a separate resource that does not exist in our framework.
19 | - **Base** – Represents all our controls’ backplates and their temporary state visuals like hover or press. In general, Base should be in contrast with the background (or Region color) of your theme and with black text (if in Light theme) and white text (if in Dark theme).
20 | - **Primary** – This is essentially the Accent color and should contrast with mainly white text. It is also used in more choice locations to show alternate rest states for toggled controls like list selection, checkbox or radiobutton checked states, slider fill values, and other control parts that need to be shown as different from their default rest state once interacted with.
21 |
22 | Refining the colors
23 | ---
24 | In addition to the three major colors for each theme, you can also expand any one of the major colors to see a list of minor colors that change the look of only certain control parts - this basically allows you to get more detailed with your color choices for states.
25 |
26 | 
27 |
28 | To access the detailed view of colors, simply click the chevron next to the major color button swatches.
29 |
30 | Creating, saving and loading presets
31 | ---
32 | The editor will ship with some presets for you to look at to get an idea of what a theme looks like in the app. The preset dropdown is located at the top of the Color Dictionary properties panel.
33 |
34 | When you first boot up it will always be set to Default – which is the Light and Dark theme styling default for all our controls. You can select different themes like Lavender and Nighttime to get an idea of how the tool will theme our controls.
35 |
36 | Once you’re ready to start making your own theme, just start editing the colors! Once you’ve started tweaking them, you’ll notice that the Presets ComboBox goes from the name of the preset to “Custom”:
37 |
38 | This means that you’ve started a new temporary theme that’s “Custom.” Any changes you make will not affect any of the other Presets in that box.
39 |
40 | - Once you’re satisfied with the changes you’ve made, simply click the “Save” button and browse to your desired save point.
41 | - Similarly, you can open your saved JSON theme by clicking the “Load” button and browsing to your saved theme’s file location.
42 |
43 | Checking contrast ratio
44 | ---
45 | The last part of the theme editor is probably one of the most important parts of creating your theme, and that is to make sure that in either respective theme you are contrast compliant. The tool provides you with a small list of contrast information on the left-hand side of the color selection window when choosing your color.
46 |
47 | 
48 |
49 | In this window you can see your contrast with the most prevalent text color in the theme that you’re choosing to edit, in the above case black text because you are editing a Light theme color value.
50 |
51 | When you pick a color that falls below the standard contrast ratio of **4.5:1**, you’ll be alerted with red text next to your contrast value.
52 |
53 | 
54 |
55 | You can learn more about [contrast ratios and their importance here](https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/accessible-text-requirements).
56 |
57 | Exporting and using your theme in a Avalonia app
58 | ---
59 | Once you’ve themed everything, you’ll want to use it in your app! To do that you’ll need to click the “Export” button at the bottom of the Color Dictionary properties panel.
60 |
61 | 
62 |
63 | That button will open a popup window with a generic FluentTheme definition that contains your palette.
64 |
65 | Once you’re ready to use it in your app, click the “Copy to Clipboard” button in the lower right corner and go to Avalonia App.axaml file.
66 |
67 | Normally, your App.axaml file will have FluentTheme defined already. It uses build-in dark and light palettes.
68 | Note, your App.axaml might contain additional lines like DataGrid or ColorPicker include or more.
69 | To update FluentTheme palettes we only need to update FluentTheme node.
70 | ```xaml
71 |
76 |
77 |
78 |
79 |
80 | ```
81 |
82 | Now you can paste the exported theme code from the editor replacing default ``:
83 | ```xaml
84 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | ```
99 |
100 | Once that’s in, you’re done! Your theme colors will now be pervasive across your app or page depending.
101 |
--------------------------------------------------------------------------------
/README_Images/App.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/App.png
--------------------------------------------------------------------------------
/README_Images/ColorContrast_Bad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/ColorContrast_Bad.png
--------------------------------------------------------------------------------
/README_Images/ColorContrast_Good.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/ColorContrast_Good.png
--------------------------------------------------------------------------------
/README_Images/ExportTheme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/ExportTheme.png
--------------------------------------------------------------------------------
/README_Images/RegionBasePrimary_DetailColorProperties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/RegionBasePrimary_DetailColorProperties.png
--------------------------------------------------------------------------------
/README_Images/RegionBasePrimary_Properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AvaloniaUI/fluent-xaml-theme-editor/e8e61182439ebc4bb034568db44654b2feb6efb6/README_Images/RegionBasePrimary_Properties.png
--------------------------------------------------------------------------------