ci: compile the manual
[nit.git] / lib / ios / ui / ui.nit
index 96d1e8a..da04397 100644 (file)
@@ -25,16 +25,16 @@ in "ObjC" `{
 @interface NitCallbackReference: NSObject
 
        // Nit object target of the callbacks from UI events
-       @property (nonatomic) Button nit_button;
+       @property (nonatomic) View nit_view;
 
        // Actual callback method
-       -(void) nitOnEvent: (UIButton*) sender;
+       -(void) nitOnEvent: (UIView*) sender;
 @end
 
 @implementation NitCallbackReference
 
-       -(void) nitOnEvent: (UIButton*) sender {
-               Button_on_click(self.nit_button);
+       -(void) nitOnEvent: (UIView*) sender {
+               View_on_ios_event(self.nit_view);
        }
 @end
 
@@ -42,7 +42,7 @@ in "ObjC" `{
 @interface UITableViewAndDataSource: NSObject <UITableViewDelegate, UITableViewDataSource>
 
        // Nit object receiving the callbacks
-       @property ListLayout nit_list_layout;
+       @property TableView nit_list_layout;
 
        // List of native views added to this list view from the Nit side
        @property NSMutableArray *views;
@@ -58,36 +58,102 @@ in "ObjC" `{
        }
 
        - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-               return ListLayout_number_of_sections_in_table_view(self.nit_list_layout, tableView);
+               return TableView_number_of_sections_in_table_view(self.nit_list_layout, tableView);
        }
 
        - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-               return ListLayout_number_of_rows_in_section(self.nit_list_layout, tableView, section);
+               return TableView_number_of_rows_in_section(self.nit_list_layout, tableView, section);
        }
 
        - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-               return ListLayout_title_for_header_in_section(self.nit_list_layout, tableView, section);
+               return TableView_title_for_header_in_section(self.nit_list_layout, tableView, section);
        }
 
        - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-               return ListLayout_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath);
+               return TableView_cell_for_row_at_index_path(self.nit_list_layout, tableView, indexPath);
+       }
+@end
+
+// View controller associated to an app.nit Window
+@interface NitViewController: UIViewController
+@end
+
+@implementation NitViewController
+       - (void)viewWillDisappear:(BOOL)animated {
+               [super viewWillDisappear:animated];
+
+               if (self.isMovingFromParentViewController || self.isBeingDismissed) {
+                       extern App app_nit_ios_app;
+                       App_after_window_pop(app_nit_ios_app);
+               }
        }
 @end
 `}
 
 redef class App
+
        redef fun did_finish_launching_with_options
        do
+               app_delegate.window = new UIWindow
+               app_delegate.window.background_color = new UIColor.white_color
                super
-               window.native.make_key_and_visible
+               app_delegate.window.make_key_and_visible
                return true
        end
 
-       redef fun window=(window)
+       private fun set_view_controller(window: UIWindow, native: UIViewController)
+       in "ObjC" `{
+               // Set the required root view controller
+               UINavigationController *navController = (UINavigationController*)window.rootViewController;
+
+               if (navController == NULL) {
+                       navController = [[UINavigationController alloc]initWithRootViewController:native];
+                       navController.edgesForExtendedLayout = UIRectEdgeNone;
+
+                       // Must be non-translucent for the controls to be placed under
+                       // (as in Y axis) of the navigation bar.
+                       navController.navigationBar.translucent = NO;
+
+                       window.rootViewController = navController;
+               }
+               else {
+                       [navController pushViewController:native animated:YES];
+               }
+
+               native.edgesForExtendedLayout = UIRectEdgeNone;
+       `}
+
+       redef fun push_window(window)
+       do
+               set_view_controller(app_delegate.window, window.native)
+               super
+       end
+
+       # Use iOS ` popViewControllerAnimated`
+       redef fun pop_window
        do
-               app_delegate.window = window.native
+               manual_pop = true
+               pop_view_controller app_delegate.window
                super
        end
+
+       private fun pop_view_controller(window: UIWindow) in "ObjC" `{
+               UINavigationController *navController = (UINavigationController*)window.rootViewController;
+               [navController popViewControllerAnimated: YES];
+       `}
+
+       # Is the next `after_window_pop`  triggered by a call to `pop_window`?
+       #
+       # Otherwise, it's by the user via the navigation bar "Back" button.
+       private var manual_pop = false
+
+       # Callback when `window` is displayed again
+       private fun after_window_pop
+       do
+               if not manual_pop then window_stack.pop
+               manual_pop = false
+               window.on_resume
+       end
 end
 
 redef class AppDelegate
@@ -112,6 +178,8 @@ redef class View
        redef type NATIVE: UIView
 
        redef var enabled = null is lazy
+
+       private fun on_ios_event do end
 end
 
 redef class CompositeControl
@@ -120,18 +188,31 @@ redef class CompositeControl
        do
                super
 
-               var native_view = view.native
-               assert native_view isa UIView
-               native_view.remove_from_superview
+               if view isa View then
+                       view.native.remove_from_superview
+               end
        end
 end
 
+# View controller associated to an app.nit `Window`
+extern class NitViewController
+       super UIViewController
+
+       new import App.after_window_pop in "ObjC" `{
+               return [[NitViewController alloc] init];
+       `}
+end
+
 redef class Window
 
-       redef type NATIVE: UIWindow
-       redef var native = new UIWindow
+       redef type NATIVE: NitViewController
+       redef var native = new NitViewController
 
-       init do native.background_color = new UIColor.white_color
+       # Title of this window
+       fun title: String do return native.title.to_s
+
+       # Set the title of this window
+       fun title=(title: String) do native.title = title.to_nsstring
 
        redef fun add(view)
        do
@@ -139,26 +220,11 @@ redef class Window
 
                var native_view = view.native
                assert native_view isa UIView
-               native.add_subview native_view
 
-               fill_whole_window_with(native_view, native)
+               if view isa ListLayout then
+                       native.view.add_subview native_view
+               else native.view = native_view
        end
-
-       private fun fill_whole_window_with(native: UIView, window: UIWindow)
-       in "ObjC" `{
-               // Hard coded borders including the top bar
-               // FIXME this may cause problems with retina devices
-               [window addConstraints:[NSLayoutConstraint
-                       constraintsWithVisualFormat: @"V:|-24-[view]-8-|"
-                       options: 0 metrics: nil views: @{@"view": native}]];
-               [window addConstraints:[NSLayoutConstraint
-                       constraintsWithVisualFormat: @"H:|-8-[view]-8-|"
-                       options: 0 metrics: nil views: @{@"view": native}]];
-
-               // Set the required root view controller
-               window.rootViewController = [[UIViewController alloc]initWithNibName:nil bundle:nil];
-               window.rootViewController.view = native;
-       `}
 end
 
 redef class Layout
@@ -169,11 +235,6 @@ redef class Layout
        init
        do
                native.alignment = new UIStackViewAlignment.fill
-               native.distribution = new UIStackViewDistribution.fill_equally
-               native.translates_autoresizing_mask_into_constraits = false
-
-               # TODO make customizable
-               native.spacing = 4.0
        end
 
        redef fun add(view)
@@ -187,11 +248,28 @@ redef class Layout
 end
 
 redef class HorizontalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.horizontal
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.horizontal
+               native.distribution = new UIStackViewDistribution.fill_equally
+       end
 end
 
 redef class VerticalLayout
-       redef init do native.axis = new UILayoutConstraintAxis.vertical
+       redef init
+       do
+               native.axis = new UILayoutConstraintAxis.vertical
+               native.distribution = new UIStackViewDistribution.equal_spacing
+       end
+end
+
+redef class TextView
+       # Convert `size` from app.nit relative size to iOS font points
+       private fun ios_points(size: nullable Float): Float
+       do
+               size = size or else 1.0
+               return 8.0 + size * 5.0
+       end
 end
 
 redef class Label
@@ -201,6 +279,28 @@ redef class Label
 
        redef fun text=(text) do native.text = (text or else "").to_nsstring
        redef fun text do return native.text.to_s
+
+       redef fun size=(size) do native.size = ios_points(size)
+
+       redef fun align=(align) do native.align = align or else 0.0
+end
+
+redef class UILabel
+
+       private fun size=(points: Float)
+       in "ObjC" `{
+               self.font = [UIFont systemFontOfSize: points];
+       `}
+
+       private fun align=(align: Float)
+       in "ObjC" `{
+               if (align == 0.5)
+                       self.textAlignment = NSTextAlignmentCenter;
+               else if (align < 0.5)
+                       self.textAlignment = NSTextAlignmentLeft;
+               else//if (align > 0.5)
+                       self.textAlignment = NSTextAlignmentRight;
+       `}
 end
 
 # On iOS, check boxes are a layout composed of a label and an `UISwitch`
@@ -218,15 +318,20 @@ redef class CheckBox
        # `UISwitch` acting as the real check box
        var ui_switch: UISwitch is noautoinit
 
-       init do
+       redef fun on_ios_event do notify_observers new ToggleEvent(self)
+
+       init
+       do
                # Tweak the layout so it is centered
-               layout.native.distribution = new UIStackViewDistribution.fill_proportionally
-               layout.native.alignment = new UIStackViewAlignment.center
+               layout.native.distribution = new UIStackViewDistribution.equal_spacing
+               layout.native.alignment = new UIStackViewAlignment.fill
                layout.native.layout_margins_relative_arrangement = true
 
                var s = new UISwitch
                native.add_arranged_subview s
                ui_switch = s
+
+               ui_switch.set_callback self
        end
 
        redef fun text=(text) do lbl.text = text
@@ -236,6 +341,23 @@ redef class CheckBox
        redef fun is_checked=(value) do ui_switch.set_on_animated(value, true)
 end
 
+redef class UISwitch
+       # Register callbacks on this switch to be relayed to `sender`
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
+
+               NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
+               ncr.nit_view = sender;
+
+               // Pin the objects in both Objective-C and Nit GC
+               View_incr_ref(sender);
+               ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
+
+               [self addTarget:ncr action:@selector(nitOnEvent:)
+                       forControlEvents:UIControlEventValueChanged];
+       `}
+end
+
 redef class TextInput
 
        redef type NATIVE: UITextField
@@ -249,6 +371,31 @@ redef class TextInput
                native.secure_text_entry = value or else false
                super
        end
+
+       redef fun size=(size) do native.size = ios_points(size)
+
+       redef fun align=(align) do native.align = align or else 0.0
+
+       # Set the placeholder text, shown in light gray when the field is empty
+       fun placeholder=(text: Text) do native.placeholder = text.to_nsstring
+end
+
+redef class UITextField
+
+       private fun size=(points: Float)
+       in "ObjC" `{
+               self.font = [UIFont systemFontOfSize: points];
+       `}
+
+       private fun align=(align: Float)
+       in "ObjC" `{
+               if (align == 0.5)
+                       self.textAlignment = NSTextAlignmentCenter;
+               else if (align < 0.5)
+                       self.textAlignment = NSTextAlignmentLeft;
+               else//if (align > 0.5)
+                       self.textAlignment = NSTextAlignmentRight;
+       `}
 end
 
 redef class Button
@@ -258,25 +405,29 @@ redef class Button
 
        init do native.set_callback self
 
+       redef fun on_ios_event do notify_observers new ButtonPressEvent(self)
+
        redef fun text=(text) do if text != null then native.title = text.to_nsstring
        redef fun text do return native.current_title.to_s
 
-       private fun on_click do notify_observers new ButtonPressEvent(self)
-
        redef fun enabled=(enabled) do native.enabled = enabled or else true
        redef fun enabled do return native.enabled
+
+       redef fun size=(size) do native.title_label.size = ios_points(size)
+
+       redef fun align=(align) do native.title_label.align = align or else 0.0
 end
 
 redef class UIButton
        # Register callbacks on this button to be relayed to `sender`
-       private fun set_callback(sender: Button)
-       import Button.on_click in "ObjC" `{
+       private fun set_callback(sender: View)
+       import View.on_ios_event in "ObjC" `{
 
                NitCallbackReference *ncr = [[NitCallbackReference alloc] init];
-               ncr.nit_button = sender;
+               ncr.nit_view = sender;
 
                // Pin the objects in both Objective-C and Nit GC
-               Button_incr_ref(sender);
+               View_incr_ref(sender);
                ncr = (__bridge NitCallbackReference*)CFBridgingRetain(ncr);
 
                [self addTarget:ncr action:@selector(nitOnEvent:)
@@ -284,8 +435,70 @@ redef class UIButton
        `}
 end
 
+# On iOS, implemented by a `UIStackView` inside a ` UIScrollView`
 redef class ListLayout
 
+       redef type NATIVE: UIScrollView
+       redef var native = new UIScrollView
+
+       # Real container of the subviews, contained within `native`
+       var native_stack_view = new UIStackView
+
+       redef fun parent=(parent)
+       do
+               super
+
+               var root_view
+               if parent isa Window then
+                       root_view = parent.native.view
+               else if parent isa View then
+                       root_view = parent.native
+               else return
+
+               # Setup scroll view
+               var native_scroll_view = native
+               native_scroll_view.translates_autoresizing_mask_into_constraits = false
+               native_add_constraints(root_view, native_scroll_view)
+
+               # Setup stack_view
+               native_stack_view.translates_autoresizing_mask_into_constraits = false
+               native_stack_view.axis = new UILayoutConstraintAxis.vertical
+               native_scroll_view.add_subview native_stack_view
+               native_add_constraints(native_scroll_view, native_stack_view)
+               native_lock_vertical_scroll(native_scroll_view, native_stack_view)
+       end
+
+       # Add constraints to lock the vertical and horizontal dimensions
+       private fun native_add_constraints(root_view: UIView, nested_view: UIView)
+       in "ObjC" `{
+               [root_view addConstraints:[NSLayoutConstraint
+                       constraintsWithVisualFormat: @"V:|-0-[nested_view]-0-|"
+                       options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"nested_view": nested_view}]];
+               [root_view addConstraints:[NSLayoutConstraint
+                       constraintsWithVisualFormat: @"H:|-0-[nested_view]-0-|"
+                       options: NSLayoutFormatAlignAllCenterX metrics: nil views: @{@"nested_view": nested_view}]];
+       `}
+
+       # Add a constraint to lock to the scroll vertically
+       private fun native_lock_vertical_scroll(scroll_view: UIScrollView, stack_view: UIStackView)
+       in "ObjC" `{
+               [scroll_view addConstraint: [scroll_view.widthAnchor constraintEqualToAnchor:stack_view.widthAnchor]];
+       `}
+
+       redef fun add(view)
+       do
+               super
+
+               if view isa View then
+                       native_stack_view.add_arranged_subview view.native
+               end
+       end
+end
+
+# iOS specific layout using a `UITableView`, works only with simple children views
+class TableView
+       super CompositeControl
+
        redef type NATIVE: UITableView
        redef var native = new UITableView(new UITableViewStyle.plain)
 
@@ -385,20 +598,32 @@ end
 redef class UITableView
 
        # Assign `list_view` as `delegate` and `dataSource`, and pin all references in both GCs
-       private fun assign_delegate_and_data_source(list_view: ListLayout)
-       import ListLayout.number_of_sections_in_table_view,
-              ListLayout.number_of_rows_in_section,
-              ListLayout.title_for_header_in_section,
-              ListLayout.cell_for_row_at_index_path in "ObjC" `{
+       private fun assign_delegate_and_data_source(list_view: TableView)
+       import TableView.number_of_sections_in_table_view,
+              TableView.number_of_rows_in_section,
+              TableView.title_for_header_in_section,
+              TableView.cell_for_row_at_index_path in "ObjC" `{
 
                UITableViewAndDataSource *objc_delegate = [[UITableViewAndDataSource alloc] init];
                objc_delegate = (__bridge UITableViewAndDataSource*)CFBridgingRetain(objc_delegate);
 
                objc_delegate.nit_list_layout = list_view;
-               ListLayout_incr_ref(list_view);
+               TableView_incr_ref(list_view);
 
                // Set our
                self.delegate = objc_delegate;
                self.dataSource = objc_delegate;
        `}
 end
+
+redef class Text
+       redef fun open_in_browser do to_nsstring.native_open_in_browser
+end
+
+redef class NSString
+       private fun native_open_in_browser
+       in "ObjC" `{
+               NSURL *nsurl = [NSURL URLWithString: self];
+               [[UIApplication sharedApplication] openURL: nsurl];
+       `}
+end