DynamicWin by Florian Butz is licensed under CC BY-SA 4.0
A Dynamic Island inspired Windows App that brings in a bunch of features like widgets or a file tray that works like a clipboard.
Please note that I messed up the versioning. It is still in early development and no where near finished. v1.0.1R should be v0.0.1B and so on.
The idea for this application originally came to me when I saw the dynamic island on iPhone for the first time. After seeing that there are no (good) solutions to a Dynamic Island type application on Windows, I got the idea to make my own.
I only got the motivation to start on it though, after seeing something similar has been done on macOS already (NotchNook).
I love programming and I had the idea stuck in my mind for quite some time now.
I originally made the project in Java (luckily, that changed) but didn't get any progress since Java is less connected to the Operating system than a language like C#.
I re-made the project in WinForms getting way better results and being more motivated and then had to migrate it to WPF because of my stupid mistake of not trying if things would actually work before doing it. (:D)
WPF is a powerful UI framework, however to archive the look and feel of this app I decided on creating every UI element from scratch using SkiaSharp for the rendering. This allowed me to create an app that looks like something you would find on macOS which was perfect for this project.
I wanted to give you a peak at what went into this project and why/how I made it.
If you want to support the development of this project you can create pull requests and contribute to it or make extensions / themes and upload them on the discord :)
Feel free to open up a new issue if you encounter any bugs / flaws.
Quick disclaimer: The code is terribly structured and almost un-maintainable as it was only meant to be a small side project so please don't expect too much from me :)
Note
Only the checked features are currently available. The unchecked ones are not guaranteed to come, but are on my mind.
DynamicWin has a variety of features, currently including:
-
Ctrl + Win
Will hide the island (or show it again). -
Shift + Win
Will open a quick search menu.
- Media Playback Widget
- Timer Widget
- Weather Widget
- Voicemeeter integration Widget
- Shortcuts Widget (Can be configured to open a file, e.g. Shortcut, .EXE or any other filetype.)
- Calendar Widget
- Tuya Smart integration (Will probably be turned in to one widget with the shortcuts)
- Time Display
- Music Visualizer
- Device Usage Detector (Indicates if camera / microphone is in use)
- Power State Display (Shows battery in form of icons. If no battery is found it shows a connector icon instead)
- Timer (Displaying current running timer)
- CPU/GPU Usage Display
Files can be dragged over the island to add them to the file tray. The tray can be accessed when hovering over the island and clicking on the 'Tray' button. The files are stored until they are dragged out again. They can also be removed by selecting the file and right clicking. A context menu will popup and you can click on - "Remove Selected Files" or "Remove Selected Files" to copy the files.
Idea for the future: An implementation of a service like SnapDrop to allow for an "AirDrop" kind of feature using the file tray.
Warning
If you are using the file tray to import files in to an app (e.g. After Effects) make sure to not remove the files from the tray. Apps that only copy a link to the file will loose it after you remove the file from the tray.
The Media Playback Widget automatically detects when an instance of the Spotify app is running (Desktop version only). It will display the current playing song name and the artist. Login to the Spotify service on the app is not required.
Yes! You can add your own small widgets and big widgets by creating a custom extension!
Loading an extension from someone else is very simple. You just need to drag the Mod.dll file in to the Extensions folder that is located in the %appdata%/DynamicWin
directory.
Warning
Please never load a mod that is not tested to be safe!
Mods can contain malicious code that can mess up your system, so always check a mod's source code or let a trustworthy person check it for you.
Note
The above shown themes were generated by ChatGPT. If you want one of them, join the discord and let me know.
You can use the built-in dark / light theme. You can also create custom themes that fit your liking by going to the %appdata%/DynamicWin/Theme.json
file. After editing the colors you need to select the Custom
theme option in the settings. If you already did that, you will need to go back to the settings and click on it again. Otherwise you would have to restart the app.
This is an example of a color:
"IslandColor": "#000000"
The hex code is structured this way: #rrggbb
. If you want to change the alpha of the color, it is always at the start of the code. #aarrggbb
.
The performance might not be the best. I will try my best to add performance options to the settings menu but cannot guarantee a smooth experience for everyone.
The app might suddenly disappear and upon trying to reopen it a message box will tell you that only one instance of the app can run at the same time. To fix this, open task manager and find the process "DynamicWin". Kill it and start the app again.
Too fast interactions might confuse the animation system and will result in an empty menu. To fix this, usually moving the mouse away from the island and then over it again will fix it.
To create an extension you need an IDE like Visual Studio.
- Create a new C# Project of the type "Class Library". The target framework has to be
.NET 8
. - You will need to add at least
DynamicWin.dll
and the SkiaSharp DLLs as assembly dependencies to your project. How to add references to a VS project - Create a new C# class file, if it's not already there. Rename the class to something like "MyExtension".
- All extensions must have a class that implements the
IDynamicWinExtension
interface.
Here is an example of how the extension class should look like:
public class TestExtension : IDynamicWinExtension
{
public string AuthorName => "Florian Butz"; // The display name of the author (you) of the extension
public string ExtensionName => "Test Extension"; // The dislpay name of the extension
public string ExtensionID => "florianbutz.test"; // The ID of the extension
public List<IRegisterableWidget> GetExtensionWidgets() // Returns all Widgets that are available in this extension
{
return new List<IRegisterableWidget>() { }; // Creates a list with all IRegisterableWidgets in this extension
}
public void LoadExtension() // Gets executed after DynamicWin has finished loading in all extensions and has created the window
{
System.Diagnostics.Debug.WriteLine(ExtensionName + " was loaded sucessfully!"); // Debug text
}
}
Now you can create your first widget. Create a new class and call it how you want. I will call mine TestWidget
for now. The class has to implement from WidgetBase
if it is a big widget. If you want to make a small widget, extend the class from SmallWidgetBase
.
public class TestWidget : WidgetBase
{
DWText text; // Reference for the text object
public TestWidget(UIObject? parent, Vec2 position, UIAlignment alignment = UIAlignment.TopCenter) : base(parent, position, alignment) // Overriding the constructor is essential.
{
text = new DWText(this, "This is a Text!", Vec2.zero, UIAlignment.Center); // Creates a new text object
AddLocalObject(text); // Adds an object to the UIObject (Widget)
}
public override void DrawWidget(SKCanvas canvas) // Gets called when the widget is drawn. Do not override 'Draw()' since it contains other important things.
{
var paint = GetPaint(); // Creates a new paint of the UIObject. Please only use this and don't create your own paint.
paint.Color = Theme.Primary.Value(); // Sets the color of the paint to the current Primary color
var rect = GetRect(); // Gets the rect / bounds of the widget
canvas.DrawRoundRect(rect, paint); // Draws the rect to the screen. Only use 'DrawRoundRect()' when trying to draw the bounds of the UIObject.
}
}
DynamicWin needs to know what widgets are in your extension. To register the widget, create a new class that implements from the IRegisterableWidget
class. For readability reasons I'll call mine RegisterTestWidget
.
public class RegisterTestWidget : IRegisterableWidget
{
public bool IsSmallWidget => false; // Determins if the widget is supposed to be small or big (small widgets are displyed when the island is not hovered)
public string WidgetName => "Test Widget"; // The display name of the widget
public WidgetBase CreateWidgetInstance(UIObject? parent, Vec2 position, UIAlignment alignment = UIAlignment.TopCenter) // Overriding the constructor is essential.
{
return new TestWidget(parent, position, alignment); // Needs to return the Widget that you are trying to register
}
}
Now the extension is almost ready. As a last step, go back to your main extension class and add the RegisterTestWidget
(or however you called it) class to the GetExtensionWidgets()
function's return list.
public List<IRegisterableWidget> GetExtensionWidgets() // Returns all Widgets that are available in this extension
{
return new List<IRegisterableWidget>() { new RegisterTestWidget() }; // Creates a list with all IRegisterableWidgets in this extension
}
Now you're done. Build the project and go to your project's output folder. (Most times located under \bin\Debug\net8.0\
or \bin\Release\net8.0\
and move ONLY the DLL file that has the name of your project in to the %appdata%/DynamicWin/Extensions
folder. In this case, my output DLL is called TestExtension.dll
.
After that you can run DynamicWin and test your extension. This was of course a very bare bones example.
Tip
It's best to look at the widgets that are already in DynamicWin and learn from them.
Here is a small list of current UIObjects that can be used to make the widget creation process easier.
- Constructor
UIObject? parent
is the parent UIObject, most times the widget itself.Vec2 position
is the position of the UIObject.Vec2 size
is the size of the UIObject.UIAlignment alignment
is an optional paramenter which determins where inside the parent the object's zero point is.
- Important Methods
SetActive(bool)
sets the active state of the UIObject with an animation.SilentSetActive(bool)
sets the active state of the UIObject without an animation.SKRoundRect GetRect()
returns the bounds of the UIObject (SKRoundRect
);ContextMenu? CreateContextMenu()
is overriden if the UIObject is supposed to have a context (right click) menu using the WPFContextMenu
class.AddLocalObject(UIObject)
adds another UIObject inside the UIObject that called it. Parent is automatically set to the caller UIObject.DestroyLocalObject(UIObject obj)
removes the local UIObject.GetColor(Col)
takes in a color and returns the same color but with correct transparency. Please use it whenever you use custom colors or colors from the Theme class.
- Important Fields / Properties
IsHovering
returns true if the mouse is over the UIObject.IsMouseDown
returns true if the mouse is down over the UIObject.Color
returns the color of the object.Position
returns the position of the object.RawPosition
returns the actual position of the object without the screen transformation (use this when setting thePosition
field).Anchor
is the anchor of the object. By default set to(0.5f, 0.5f)
which means zero is in the middle of the UIObject.Size
returns the size of the object.IsEnabled
returns the active state of the UIObject.
- Constructor
string text
is the text.
- Important Methods
SetText(string)
sets the text with an animation.SilentSetText(string)
sets the text without an animation.
- Important Fields / Properties
Font
is the Typeface of the text.TextBounds
are the bounds of the text.TextSize
is the size of the text.
(Do not use. This is the base class of DWTextButton
, DWImageButton
and DWTextImageButton
)
- Constructor
Action clickCallback
is an action that gets called on click.
- Important Fields / Properties
normalColor
is the color of the button when not hovered.hoverColor
is the color of the button when hovered.clickColor
is the color of the button when clicked.colorSmoothingSpeed
is the speed at which the color updates.normalScaleMulti
is the size of the button when not hovered.hoverScaleMulti
is the size of the button when hovered.normclickScaleMultialScaleMulti
is the size of the button when clicked.
- Constructor
string buttonText
is the text on the button.
- Important Fields / Properties
normalTextSize
is the size of the text when not hovered.
- Constructor
SKBitmap image
is the image on the button.
- Important Fields / Properties
imageScale
the scale multiplier of the image. Default is0.85f
.1f
would be the scale of the button.Image
the image of the button.
- Constructor
SKBitmap image
is the text on the button.string buttonText
is the text on the button.Action clickCallback
is an action that gets called on click.
- Important Fields / Properties
imageScale
the scale multiplier of the image. Default is0.85f
.1f
would be the scale of the button.normalTextSize
is the size of the text when not hovered.Image
the image of the button.
The Res
class can be used to load in SKTypeface or SKBitmap (for DWText or DWImage).
For colors the Col
class is used. When applying the color to the paint Col.Value()
has to be called to convert to SKColor
.
Try to only use the colors available in the Theme
class to provide color customizability to the user.