├── icon.png ├── SampleApp ├── Resources │ ├── Fonts │ │ ├── OpenSans-Regular.ttf │ │ └── OpenSans-SemiBold.ttf │ ├── appicon.svg │ ├── Raw │ │ └── AboutAssets.txt │ ├── appiconfg.svg │ ├── Colors.xaml │ ├── Images │ │ └── dotnet_bot.svg │ └── Styles.xaml ├── Imports.cs ├── Properties │ └── launchSettings.json ├── Platforms │ ├── Android │ │ ├── Resources │ │ │ └── values │ │ │ │ └── colors.xml │ │ ├── AndroidManifest.xml │ │ ├── MainApplication.cs │ │ └── MainActivity.cs │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── Program.cs │ │ ├── Entitlements.plist │ │ └── Info.plist │ ├── iOS │ │ ├── AppDelegate.cs │ │ ├── Program.cs │ │ └── Info.plist │ └── Windows │ │ ├── App.xaml │ │ ├── app.manifest │ │ ├── App.xaml.cs │ │ └── Package.appxmanifest ├── Views │ ├── MainPage.xaml.cs │ ├── TestDelayedInitPage.xaml.cs │ ├── DynamicItemsPage.xaml.cs │ ├── TestDelayedInitPage.xaml │ ├── MainPage.xaml │ └── DynamicItemsPage.xaml ├── App.xaml.cs ├── ViewModels │ ├── MainPageViewModel.cs │ ├── TestDelayedInitViewModel.cs │ └── DynamicItemsPageViewModel.cs ├── Controls │ ├── DateTimePicker.xaml │ └── DateTimePicker.xaml.cs ├── App.xaml ├── MauiProgram.cs └── SampleApp.csproj ├── Vapolia.SegmentedViews ├── WidthDefinitionCollection.cs ├── ISegmentedView.cs ├── SegmentExtensions.cs ├── Segment.cs ├── FilenameBasedMultiTargeting.props ├── WidthDefinitionCollectionTypeConverter.cs ├── MauiAppBuilderExtensions.cs ├── Vapolia.SegmentedViews.csproj ├── WeakEventManager.cs ├── SegmentedViewHandler.windows.cs ├── SegmentedViewHandler.macios.cs ├── SegmentedViewHandler.android.cs └── SegmentedView.cs ├── .run ├── Windows.run.xml ├── iOS.run.xml └── Android.run.xml ├── .github └── workflows │ └── main.yaml ├── Vapolia.SegmentedViews.sln ├── README.md ├── .gitignore └── LICENSE.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapolia/SegmentedViews/HEAD/icon.png -------------------------------------------------------------------------------- /SampleApp/Resources/Fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapolia/SegmentedViews/HEAD/SampleApp/Resources/Fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /SampleApp/Resources/Fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapolia/SegmentedViews/HEAD/SampleApp/Resources/Fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /SampleApp/Imports.cs: -------------------------------------------------------------------------------- 1 | global using SampleApp; 2 | global using SampleApp.Controls; 3 | 4 | // Static 5 | global using static Microsoft.Maui.Graphics.Colors; 6 | -------------------------------------------------------------------------------- /SampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "Windows Machine": { 5 | "commandName": "MsixPackage", 6 | "nativeDebugging": false 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SampleApp/Resources/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #512BD4 4 | #2B0B98 5 | #2B0B98 6 | -------------------------------------------------------------------------------- /SampleApp/Views/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using SampleApp.ViewModels; 2 | 3 | namespace SampleApp.Views; 4 | 5 | public partial class MainPage : ContentPage 6 | { 7 | public MainPage() 8 | { 9 | InitializeComponent(); 10 | BindingContext = new MainPageViewModel(Navigation); 11 | } 12 | } -------------------------------------------------------------------------------- /SampleApp/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace SampleApp 4 | { 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SampleApp/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace SampleApp 4 | { 5 | [Register(nameof(AppDelegate))] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SampleApp/Views/TestDelayedInitPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using SampleApp.ViewModels; 2 | 3 | namespace SampleApp.Views; 4 | 5 | public partial class TestDelayedInitPage 6 | { 7 | public TestDelayedInitPage() 8 | { 9 | InitializeComponent(); 10 | BindingContext = new TestDelayedInitViewModel(); 11 | } 12 | } -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/WidthDefinitionCollection.cs: -------------------------------------------------------------------------------- 1 | namespace Vapolia.SegmentedViews; 2 | 3 | public class WidthDefinitionCollection : List 4 | { 5 | public WidthDefinitionCollection() {} 6 | 7 | public WidthDefinitionCollection(IEnumerable definitions) : base(definitions) 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /SampleApp/Views/DynamicItemsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using SampleApp.ViewModels; 2 | 3 | namespace SampleApp.Views; 4 | 5 | public partial class DynamicItemsPage : ContentPage 6 | { 7 | public DynamicItemsPage() 8 | { 9 | BindingContext = new DynamicItemsPageViewModel(Navigation); 10 | InitializeComponent(); 11 | } 12 | } -------------------------------------------------------------------------------- /SampleApp/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.run/Windows.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SampleApp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using SampleApp.Views; 2 | 3 | namespace SampleApp; 4 | 5 | public partial class App : Application 6 | { 7 | public App() 8 | { 9 | InitializeComponent(); 10 | UserAppTheme = PlatformAppTheme; 11 | } 12 | 13 | protected override Window CreateWindow(IActivationState? activationState) 14 | { 15 | return new Window(new NavigationPage(new MainPage())); 16 | } 17 | } -------------------------------------------------------------------------------- /SampleApp/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace SampleApp 5 | { 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | using Android.OS; 4 | 5 | namespace SampleApp 6 | { 7 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] 8 | public class MainActivity : MauiAppCompatActivity 9 | { 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SampleApp/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace SampleApp 5 | { 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SampleApp/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using ObjCRuntime; 2 | using UIKit; 3 | 4 | namespace SampleApp 5 | { 6 | public class Program 7 | { 8 | // This is the main entry point of the application. 9 | static void Main(string[] args) 10 | { 11 | // if you want to use a different Application Delegate class from "AppDelegate" 12 | // you can specify it here. 13 | UIApplication.Main(args, null, typeof(AppDelegate)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.run/iOS.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.run/Android.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /SampleApp/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "MauiAsset": 3 | 4 | 5 | 6 | These files will be deployed with you package and will be accessible using Essentials: 7 | 8 | async Task LoadMauiAsset() 9 | { 10 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 11 | using var reader = new StreamReader(stream); 12 | 13 | var contents = reader.ReadToEnd(); 14 | } 15 | -------------------------------------------------------------------------------- /SampleApp/Platforms/MacCatalyst/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | com.apple.security.app-sandbox 8 | 9 | 10 | com.apple.security.network.client 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SampleApp/ViewModels/MainPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Input; 2 | using SampleApp.Views; 3 | 4 | namespace SampleApp.ViewModels; 5 | 6 | public class MainPageViewModel 7 | { 8 | public int SegmentSelectedIndex { get; set; } 9 | public ICommand SegmentSelectionChangedCommand { get; } 10 | public ICommand GoAdvancedDemoPageCommand { get; } 11 | 12 | public MainPageViewModel(INavigation navigation) 13 | { 14 | SegmentSelectionChangedCommand = new Command(() => 15 | { 16 | var selectedItem = SegmentSelectedIndex; 17 | //... 18 | }); 19 | 20 | GoAdvancedDemoPageCommand = new Command(() => 21 | { 22 | navigation.PushAsync(new DynamicItemsPage()); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SampleApp/Controls/DateTimePicker.xaml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SampleApp/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /SampleApp/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Vapolia.SegmentedViews; 3 | 4 | namespace SampleApp 5 | { 6 | public static partial class MauiProgram 7 | { 8 | public static MauiApp CreateMauiApp() 9 | { 10 | var builder = MauiApp.CreateBuilder(); 11 | builder.UseMauiApp() 12 | .ConfigureFonts(fonts => 13 | { 14 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); 15 | fonts.AddFont("OpenSans-SemiBold.ttf", "OpenSansSemiBold"); 16 | }) 17 | .UseSegmentedView(); 18 | 19 | #if DEBUG 20 | builder.Logging.AddDebug(); 21 | #endif 22 | 23 | return builder.Build(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/ISegmentedView.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Vapolia.SegmentedViews; 4 | 5 | public interface ISegmentedView : IView, ITextStyle 6 | { 7 | public Color SelectedBackgroundColor { get; } 8 | public Color SelectedTextColor { get; } 9 | public Color DisabledColor { get; } 10 | public Color BackgroundColor { get; } 11 | public Color BorderColor { get; } 12 | // public double BorderWidth { get; } 13 | public Thickness ItemPadding { get; set; } 14 | 15 | public int SelectedIndex { get; } 16 | internal void SetSelectedIndex(int i); 17 | public bool IsSelectionRequired { get; } 18 | 19 | //internal string? TextPropertyName { get; } 20 | internal IValueConverter? TextConverter { get; } 21 | internal ObservableCollection Children { get; } 22 | internal WidthDefinitionCollection? WidthDefinitions { get; } 23 | internal GridLength ItemsDefaultWidth { get; } 24 | } 25 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace SampleApp.WinUI; 7 | 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | UnhandledException += (sender, e) => 20 | { 21 | if (global::System.Diagnostics.Debugger.IsAttached) 22 | global::System.Diagnostics.Debugger.Break(); 23 | }; 24 | 25 | this.InitializeComponent(); 26 | } 27 | 28 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 29 | } -------------------------------------------------------------------------------- /SampleApp/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/SegmentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Globalization; 3 | 4 | namespace Vapolia.SegmentedViews; 5 | 6 | internal static class SegmentExtensions 7 | { 8 | public static string GetText(this Segment segment, ISegmentedView segmentedControl) 9 | { 10 | if (segment.Item == null) 11 | return string.Empty; 12 | 13 | //var obj = segmentedControl.TextPropertyName != null ? segment.Item.GetType().GetProperty(segmentedControl.TextPropertyName)?.GetValue(segment.Item) : segment.Item; 14 | var obj = segment.Item; 15 | 16 | if (segmentedControl.TextConverter != null) 17 | obj = segmentedControl.TextConverter.Convert(obj, typeof(string), null, CultureInfo.CurrentCulture); 18 | 19 | return obj?.ToString() ?? string.Empty; 20 | } 21 | 22 | public static List GetWidths(this ISegmentedView segmentedView) 23 | { 24 | return segmentedView.Children.Select((segment,i) => 25 | { 26 | if (segment.Width != null) 27 | return segment.Width.Value; 28 | if(segmentedView.WidthDefinitions?.Count > i) 29 | return segmentedView.WidthDefinitions[i]; 30 | return segmentedView.ItemsDefaultWidth; 31 | }).ToList(); 32 | } 33 | } -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/Segment.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Vapolia.SegmentedViews; 4 | 5 | public class Segment : BindableObject 6 | { 7 | public static readonly BindableProperty ItemProperty = BindableProperty.Create(nameof (Item), typeof (object), typeof (Segment), propertyChanged: (bindable, value, newValue) => ((Segment)bindable).OnItemChanged(value, newValue)); 8 | public static readonly BindableProperty WidthProperty = BindableProperty.Create(nameof (Width), typeof (GridLength?), typeof (Segment)); 9 | 10 | public object? Item 11 | { 12 | get => GetValue(ItemProperty); 13 | set => SetValue(ItemProperty, value); 14 | } 15 | 16 | [TypeConverter(typeof(GridLengthTypeConverter))] 17 | public GridLength? Width 18 | { 19 | get => (GridLength?)GetValue(WidthProperty); 20 | set => SetValue(WidthProperty, value); 21 | } 22 | 23 | private void OnItemChanged(object value, object newValue) 24 | { 25 | if (value is INotifyPropertyChanged notifyPropertyChanged1) 26 | WeakEventManager.Unsubscribe(notifyPropertyChanged1, this, OnItemPropertyChanged); 27 | 28 | if (newValue is INotifyPropertyChanged notifyPropertyChanged2) 29 | WeakEventManager.Subscribe(notifyPropertyChanged2, this, OnItemPropertyChanged); 30 | } 31 | 32 | //Simulate the change of the whole item when an item's property has changed 33 | private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) 34 | { 35 | if(e.PropertyName != nameof(Item)) 36 | OnPropertyChanged(nameof(Item)); 37 | } 38 | 39 | /// 40 | /// Finalizer to ensure weak event cleanup 41 | /// 42 | ~Segment() 43 | { 44 | WeakEventManager.UnsubscribeAll(this); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SampleApp/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | UIDeviceFamily 15 | 16 | 2 17 | 18 | UIRequiredDeviceCapabilities 19 | 20 | arm64 21 | 22 | UISupportedInterfaceOrientations 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationLandscapeLeft 26 | UIInterfaceOrientationLandscapeRight 27 | 28 | UISupportedInterfaceOrientations~ipad 29 | 30 | UIInterfaceOrientationPortrait 31 | UIInterfaceOrientationPortraitUpsideDown 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | XSAppIconAssets 36 | Assets.xcassets/appicon.appiconset 37 | 38 | 39 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/FilenameBasedMultiTargeting.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/WidthDefinitionCollectionTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Globalization; 3 | 4 | namespace Vapolia.SegmentedViews; 5 | 6 | public class WidthDefinitionCollectionTypeConverter : TypeConverter 7 | { 8 | public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) 9 | => sourceType == typeof(string); 10 | public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) 11 | => destinationType == typeof(string); 12 | 13 | public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value) 14 | { 15 | var strValue = value?.ToString(); 16 | 17 | if (strValue == null) 18 | throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(WidthDefinitionCollection)}"); 19 | 20 | var converter = new GridLengthTypeConverter(); 21 | var definitions = strValue.Split(',').Select(length => (GridLength?)converter.ConvertFromInvariantString(length)).ToList(); 22 | if(definitions.Any(d => d == null)) 23 | throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(WidthDefinitionCollection)}"); 24 | 25 | return new WidthDefinitionCollection(definitions.Cast()); 26 | } 27 | 28 | 29 | public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) 30 | { 31 | if (value is not WidthDefinitionCollection cdc) 32 | throw new NotSupportedException(); 33 | var converter = new GridLengthTypeConverter(); 34 | return string.Join(", ", cdc.Select(cd => converter.ConvertToInvariantString(cd))); 35 | } 36 | } -------------------------------------------------------------------------------- /SampleApp/Resources/appiconfg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/MauiAppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.Versioning; 3 | 4 | [assembly: XmlnsDefinition("https://vapolia.eu/Vapolia.SegmentedViews", "Vapolia.SegmentedViews", AssemblyName = "Vapolia.SegmentedViews")] 5 | [assembly: Microsoft.Maui.Controls.XmlnsPrefix("https://vapolia.eu/Vapolia.SegmentedViews", "segmented")] 6 | [assembly: Microsoft.Maui.Controls.XmlnsPrefix("clr-namespace:Vapolia.SegmentedViews;assembly=Vapolia.SegmentedViews", "segmented")] 7 | 8 | namespace Vapolia.SegmentedViews; 9 | 10 | [SuppressMessage("Usage", "CA2255: ’ModuleInitializer’ warning")] 11 | [SupportedOSPlatform("iOS15.0")] 12 | [SupportedOSPlatform("MacCatalyst14.0")] 13 | [SupportedOSPlatform("Android27.0")] 14 | [SupportedOSPlatform("Windows10.0.19041")] 15 | //[SupportedOSPlatform("Tizen6.5")] 16 | public static class MauiAppBuilderExtensions 17 | { 18 | /// 19 | /// Try to fix ns not found for Segment (but not SegmentedView) ?! 20 | /// When using https://vapolia.eu/Vapolia.SegmentedViews instead of clr-namespace:Vapolia.SegmentedViews;assembly=Vapolia.SegmentedViews 21 | /// 22 | public static Vapolia.SegmentedViews.Segment InternalSegment; 23 | 24 | /// 25 | /// Add Maui handlers for this control 26 | /// 27 | public static MauiAppBuilder UseSegmentedView(this MauiAppBuilder builder) 28 | { 29 | //?! Try to fix ns not found for Segment (but not SegmentedView) 30 | InternalSegment = new (); 31 | 32 | builder.ConfigureMauiHandlers(handlers => 33 | { 34 | #if ANDROID 35 | handlers.TryAddHandler(); 36 | #elif IOS || MACCATALYST 37 | handlers.TryAddHandler(); 38 | #elif WINDOWS 39 | handlers.TryAddHandler(); 40 | #endif 41 | }); 42 | 43 | return builder; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Publish To Nuget 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | # pull_request: 7 | # push: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | #Needs windows to build the windows version 15 | runs-on: windows-latest 16 | env: 17 | NUPKG_MAJOR: 1.0.9 18 | DOTNET_CLI_TELEMETRY_OPTOUT: true 19 | DOTNET_NOLOGO: true 20 | PROJECT: Vapolia.SegmentedViews/Vapolia.SegmentedViews.csproj 21 | # CODESIGN_PFX: ${{ secrets.CODESIGN_PFX }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | #Saves a nuget config 26 | - name: Setup .NET 27 | uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: 9.0.x 30 | 31 | # - name: Install MAUI workload 32 | # run: dotnet workload install maui 33 | 34 | - name: Build 35 | shell: pwsh 36 | run: dotnet build -c Release $env:PROJECT 37 | 38 | - name: Package & Publish NuGets 39 | shell: pwsh 40 | env: 41 | #required so if it contains special characters they are not interpreted by powershell 42 | NUGET_AUTH_TOKEN: ${{secrets.NUGETAPIKEY}} 43 | NUGET_TARGET: https://api.nuget.org/v3/index.json 44 | run: | 45 | $VERSION="$env:NUPKG_MAJOR-ci$env:GITHUB_RUN_ID" 46 | if ($env:GITHUB_EVENT_NAME -eq "release") { 47 | $VERSION = $env:GITHUB_REF.Substring($env:GITHUB_REF.LastIndexOf('/') + 1) 48 | } 49 | echo "PACKAGE VERSION: $VERSION" 50 | New-Item -ItemType Directory -Force -Path ./artifacts 51 | 52 | dotnet pack --no-build --output ./artifacts -c Release -p:PackageVersion=$VERSION $env:PROJECT 53 | # needs to CD because nuget push can't find nuget packages with a linux style path 54 | cd ./artifacts 55 | dotnet nuget push *.nupkg --skip-duplicate -k $env:NUGET_AUTH_TOKEN -s $env:NUGET_TARGET 56 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34511.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vapolia.SegmentedViews", "Vapolia.SegmentedViews\Vapolia.SegmentedViews.csproj", "{64CA4E01-223F-4DB7-B244-77563DF7CA34}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "SampleApp\SampleApp.csproj", "{8980EE83-C5DF-4D73-AE28-2752BB54F259}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Publish", "Publish", "{2D8D9461-9B7F-4CE1-A744-9640C6B06053}" 11 | ProjectSection(SolutionItems) = preProject 12 | .github\workflows\main.yaml = .github\workflows\main.yaml 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {64CA4E01-223F-4DB7-B244-77563DF7CA34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {64CA4E01-223F-4DB7-B244-77563DF7CA34}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {64CA4E01-223F-4DB7-B244-77563DF7CA34}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {64CA4E01-223F-4DB7-B244-77563DF7CA34}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {8980EE83-C5DF-4D73-AE28-2752BB54F259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {8980EE83-C5DF-4D73-AE28-2752BB54F259}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {8980EE83-C5DF-4D73-AE28-2752BB54F259}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 28 | {8980EE83-C5DF-4D73-AE28-2752BB54F259}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8980EE83-C5DF-4D73-AE28-2752BB54F259}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {EE100EBC-3C29-4555-B5A5-B925FFA078CE} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SampleApp/Views/TestDelayedInitPage.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 37 | 38 | 39 | 40 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SampleApp/Controls/DateTimePicker.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace SampleApp.Controls 4 | { 5 | public partial class DateTimePicker : ContentView 6 | { 7 | public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(DateTimePicker), default(string)); 8 | 9 | public static readonly BindableProperty DateProperty = BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(DateTimePicker), default(DateTime)); 10 | 11 | public static readonly BindableProperty TimeProperty = BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(DateTimePicker), default(TimeSpan)); 12 | 13 | public static readonly BindableProperty MinimumDateProperty = BindableProperty.Create(nameof(MinimumDate), typeof(DateTime), typeof(DateTimePicker), default(DateTime)); 14 | 15 | public DateTimePicker() 16 | { 17 | InitializeComponent(); 18 | } 19 | 20 | public TimeSpan Time 21 | { 22 | get => (TimeSpan)GetValue(TimeProperty); 23 | set => SetValue(TimeProperty, value); 24 | } 25 | 26 | public DateTime Date 27 | { 28 | get => (DateTime)GetValue(DateProperty); 29 | set => SetValue(DateProperty, value); 30 | } 31 | 32 | public string Title 33 | { 34 | get => (string)GetValue(TitleProperty); 35 | set => SetValue(TitleProperty, value); 36 | } 37 | 38 | public DateTime MinimumDate 39 | { 40 | get => (DateTime)GetValue(MinimumDateProperty); 41 | set => SetValue(MinimumDateProperty, value); 42 | } 43 | 44 | protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") 45 | { 46 | base.OnPropertyChanged(propertyName); 47 | 48 | if (propertyName == TitleProperty.PropertyName) 49 | { 50 | lblTitle.Text = Title; 51 | } 52 | else if (propertyName == DateProperty.PropertyName) 53 | { 54 | occursOn.Date = Date; 55 | } 56 | else if (propertyName == TimeProperty.PropertyName) 57 | { 58 | occursAt.Time = Time; 59 | } 60 | else if (propertyName == MinimumDateProperty.PropertyName) 61 | { 62 | occursOn.MinimumDate = MinimumDate; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SampleApp/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | $placeholder$ 17 | User Name 18 | $placeholder$.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /SampleApp/ViewModels/TestDelayedInitViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using System.Windows.Input; 4 | 5 | namespace SampleApp.ViewModels; 6 | 7 | public class TestDelayedInitViewModel : INotifyPropertyChanged 8 | { 9 | private string infoText = string.Empty; 10 | private string infoText2 = string.Empty; 11 | private object? segmentSelectedItem; 12 | private int nextInt = 42; 13 | private List persons; 14 | 15 | public object? SegmentSelectedItem 16 | { 17 | get => segmentSelectedItem; 18 | set { segmentSelectedItem = value; OnPropertyChanged(); } 19 | } 20 | 21 | public ICommand SegmentSelectionChangedCommand { get; } 22 | 23 | public string InfoText 24 | { 25 | get => infoText; 26 | set { infoText = value; OnPropertyChanged(); } 27 | } 28 | public string InfoText2 29 | { 30 | get => infoText2; 31 | set { infoText2 = value; OnPropertyChanged(); } 32 | } 33 | 34 | public ICommand AddItemCommand { get; } 35 | public ICommand RemoveItemCommand { get; } 36 | public ICommand ClearCommand { get; } 37 | 38 | public List Persons 39 | { 40 | get => persons; 41 | private set 42 | { 43 | persons = value; 44 | OnPropertyChanged(); 45 | } 46 | } 47 | 48 | public TestDelayedInitViewModel() 49 | { 50 | //Testing: 51 | //set selected item before the items are set 52 | 53 | var thePersons = new List 54 | { 55 | new (1, "Johnny", "Halliday"), 56 | new (2, "Vanessa", "Paradis"), 57 | new (3, "Jose", "Garcia"), 58 | }; 59 | 60 | SegmentSelectedItem = thePersons[1]; 61 | 62 | MainThread.BeginInvokeOnMainThread(async () => 63 | { 64 | await Task.Delay(TimeSpan.FromSeconds(1)); 65 | Persons = thePersons; 66 | 67 | if ((Person?)SegmentSelectedItem != thePersons[1]) 68 | { 69 | Console.WriteLine("Issue with SegmentedView 🤔"); 70 | } 71 | }); 72 | 73 | SegmentSelectionChangedCommand = new Command(() => 74 | { 75 | InfoText = $"Selected item: {SegmentSelectedItem ?? "-"}"; 76 | }); 77 | 78 | AddItemCommand = new Command(() => 79 | { 80 | Persons.Add(new(999, "Any", $"One {nextInt++}")); 81 | }); 82 | 83 | RemoveItemCommand = new Command(() => 84 | { 85 | if(Persons.Any()) 86 | Persons.RemoveAt(Persons.Count-1); 87 | }); 88 | 89 | ClearCommand = new Command(() => 90 | { 91 | Persons.Clear(); 92 | }); 93 | } 94 | 95 | public event PropertyChangedEventHandler? PropertyChanged; 96 | 97 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 98 | => PropertyChanged?.Invoke(this, new(propertyName)); 99 | } -------------------------------------------------------------------------------- /SampleApp/Resources/Colors.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | #512BD4 12 | #DFD8F7 13 | #2B0B98 14 | White 15 | Black 16 | #E1E1E1 17 | #C8C8C8 18 | #ACACAC 19 | #919191 20 | #6E6E6E 21 | #404040 22 | #212121 23 | #141414 24 | 25 | #DFD8F7 26 | #2B0B98 27 | #E5E5E1 28 | #969696 29 | #505050 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | #121212 53 | White 54 | White 55 | Black 56 | 57 | #F7B548 58 | #FFD590 59 | #FFE5B9 60 | #28C2D1 61 | #7BDDEF 62 | #C3F2F4 63 | #3E8EED 64 | #72ACF1 65 | #A7CBF6 66 | 67 | 68 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0-android;net9.0-ios 5 | $(TargetFrameworks);net9.0-windows10.0.19041.0 6 | 7 | Exe 8 | 9 | 10 | true 11 | 9.0.90 12 | true 13 | 14 | 15 | enable 16 | latest 17 | enable 18 | SampleApp 19 | 20 | 21 | Segments 22 | 23 | 24 | eu.vapolia.segments 25 | 26 | 27 | 1.0 28 | 1 29 | 30 | 31 | 15.0 32 | 14.0 33 | 27.0 34 | 10.0.19041.0 35 | 10.0.19041.0 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Vapolia.SegmentedViews/Vapolia.SegmentedViews.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst 6 | $(TargetFrameworks);net9.0-windows10.0.19041.0 7 | true 8 | true 9 | enable 10 | enable 11 | true 12 | 13 | 15.0 14 | 14.0 15 | 27.0 16 | 10.0.19041.0 17 | 10.0.19041.0 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | true 33 | true 34 | 35 | 36 | 37 | 38 | 0.0.0-pre1 39 | 40 | $(DefineConstants); 41 | 42 | 43 | 44 | Vapolia.SegmentedViews 45 | Segmented control view for MAUI on iOS and Android 46 | segmented control maui 47 | Segmented control view for MAUI 48 | Powerful segmented control view for MAUI (Android, iOS) 49 | $(Version)$(VersionSuffix) 50 | SegmentedViews 51 | Vapolia 52 | Benjamin Mayrargue 53 | https://vapolia.eu 54 | en 55 | © 2024-2025 Vapolia 56 | https://github.com/vapolia/SegmentedViews 57 | false 58 | LICENSE.md 59 | README.md 60 | icon.png 61 | false 62 | https://github.com/vapolia/SegmentedViews 63 | 64 | 1.0.9: remove TextPropertyName as it uses reflection. Mark library as Trimmable. 65 | 1.0.8: fix typo in Segment which prevented the Width property to work for Segments. 66 | 1.0.7: net9.0 and Windows 67 | 1.0.6: upgrade nugets. 68 | 1.0.5: Upgrade nugets. 69 | 1.0.3: add net8.0 target for unit tests 70 | 1.0.1: Upgrade nugets. Remove dependency on maui compatibility. 71 | 1.0.0: Initial release 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /SampleApp/ViewModels/DynamicItemsPageViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.Runtime.CompilerServices; 5 | using System.Windows.Input; 6 | using SampleApp.Views; 7 | 8 | namespace SampleApp.ViewModels; 9 | 10 | public record Person(int Id, string FirstName, string LastName); 11 | 12 | /// 13 | /// Sample value converter. 14 | /// You can also override ToString() on the Person class instead of using this converter. 15 | /// 16 | public class PersonTextConverter : IValueConverter 17 | { 18 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 19 | { 20 | var person = (Person)value!; 21 | return $"{person.FirstName} {person.LastName}"; 22 | } 23 | 24 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 25 | => throw new NotSupportedException(); 26 | } 27 | 28 | public class DynamicItemsPageViewModel : INotifyPropertyChanged 29 | { 30 | private string infoText = string.Empty; 31 | private string infoText2 = string.Empty; 32 | private object? segmentSelectedItem; 33 | private int nextInt = 42; 34 | private int selectedIndexChangedCallCount = 0; 35 | 36 | public object? SegmentSelectedItem 37 | { 38 | get => segmentSelectedItem; 39 | set { segmentSelectedItem = value; OnPropertyChanged(); } 40 | } 41 | 42 | public ICommand SegmentSelectionChangedCommand { get; } 43 | 44 | public string InfoText 45 | { 46 | get => infoText; 47 | set { infoText = value; OnPropertyChanged(); } 48 | } 49 | public string InfoText2 50 | { 51 | get => infoText2; 52 | set { infoText2 = value; OnPropertyChanged(); } 53 | } 54 | 55 | public ICommand AddItemCommand { get; } 56 | public ICommand RemoveItemCommand { get; } 57 | public ICommand ClearCommand { get; } 58 | public ICommand GoTestDelayedInitPageCommand { get; } 59 | public ObservableCollection Persons { get; } 60 | 61 | public int SegmentSelectedIndex 62 | { 63 | get 64 | { 65 | //Not used 66 | throw new NotSupportedException(); 67 | } 68 | set 69 | { 70 | //Only for info 71 | InfoText2 = $"Selected index #{++selectedIndexChangedCallCount}: {value}"; 72 | } 73 | } 74 | 75 | public DynamicItemsPageViewModel(INavigation navigation) 76 | { 77 | Persons = new(new Person[] 78 | { 79 | new (1, "Johnny", "Halliday"), 80 | new (2, "Vanessa", "Paradis"), 81 | new (3, "Jose", "Garcia"), 82 | }); 83 | 84 | //Testing: set selected item after a delay 85 | MainThread.BeginInvokeOnMainThread(async () => 86 | { 87 | await Task.Delay(TimeSpan.FromSeconds(3)); 88 | SegmentSelectedItem = Persons[1]; 89 | }); 90 | 91 | SegmentSelectionChangedCommand = new Command(() => 92 | { 93 | InfoText = $"Selected item: {SegmentSelectedItem ?? "-"}"; 94 | }); 95 | 96 | AddItemCommand = new Command(() => 97 | { 98 | Persons.Add(new(nextInt, "Any", $"One {nextInt++}")); 99 | }); 100 | 101 | RemoveItemCommand = new Command(() => 102 | { 103 | if(Persons.Any()) 104 | Persons.RemoveAt(Persons.Count-1); 105 | }); 106 | 107 | ClearCommand = new Command(() => 108 | { 109 | Persons.Clear(); 110 | }); 111 | 112 | 113 | GoTestDelayedInitPageCommand = new Command(() => 114 | { 115 | navigation.PushAsync(new TestDelayedInitPage()); 116 | }); 117 | } 118 | 119 | public event PropertyChangedEventHandler? PropertyChanged; 120 | 121 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 122 | => PropertyChanged?.Invoke(this, new(propertyName)); 123 | } -------------------------------------------------------------------------------- /SampleApp/Views/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 23 | 31 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 65 | 66 | 67 | 70 | 71 | 72 | 75 | 76 |