| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | //! A canvas page behind a C surface for native mobile shells: notebooks and their sections |
| 2 | | //! (`library`), one page drawn into a `CAMetalLayer`, and the active outline's text as the |
| 2 | //! (`library`), one page drawn into a native window, and the active outline's text as the |
| 3 | 3 | //! flat UTF-16 model UIKit's `UITextInput` speaks (see `TextOutline::shown_text`). Every |
| 4 | 4 | //! edit a view takes is stored at once through its section. |
| 5 | 5 | //! |
| ... | ... | @@ -616,9 +616,10 @@ struct Gpu { |
| 616 | 616 | instance: wgpu::Instance, |
| 617 | 617 | adapter: wgpu::Adapter, |
| 618 | 618 | renderer: draw::Renderer, |
| 619 | format: wgpu::TextureFormat, |
| 619 | 620 | } |
| 620 | 621 | |
| 621 | | /// Made with the first view's surface, whose adapter every later layer can use. |
| 622 | /// Made with the first view's surface and reused when the host recreates its window. |
| 622 | 623 | static GPU: Mutex<Option<Gpu>> = Mutex::new(None); |
| 623 | 624 | |
| 624 | 625 | fn gpu() -> std::sync::MutexGuard<'static, Option<Gpu>> { |
| ... | ... | @@ -677,37 +678,74 @@ pub extern "C" fn sb_set_markdown_shortcuts(on: bool) { |
| 677 | 678 | options().2 = on; |
| 678 | 679 | } |
| 679 | 680 | |
| 680 | | pub struct View { |
| 681 | | /// Taken apart on drop, which returns its text engine to `ENGINES`. |
| 682 | | canvas: ManuallyDrop<Canvas>, |
| 681 | #[cfg(target_os = "android")] |
| 682 | #[link(name = "android")] |
| 683 | unsafe extern "C" { |
| 684 | fn ANativeWindow_acquire(window: *mut c_void); |
| 685 | fn ANativeWindow_release(window: *mut c_void); |
| 686 | } |
| 687 | |
| 688 | #[cfg(target_os = "android")] |
| 689 | struct NativeWindow(std::ptr::NonNull<c_void>); |
| 690 | |
| 691 | #[cfg(target_os = "android")] |
| 692 | impl Drop for NativeWindow { |
| 693 | fn drop(&mut self) { |
| 694 | // SAFETY: this guard owns one reference acquired by `Presentation::new`. |
| 695 | unsafe { ANativeWindow_release(self.0.as_ptr()) }; |
| 696 | } |
| 697 | } |
| 698 | |
| 699 | fn view_pixels(size: [f32; 2], scale: f32, limit: u32) -> Result<[u32; 2]> { |
| 700 | if !scale.is_finite() |
| 701 | || scale <= 0.0 |
| 702 | || size.iter().any(|side| !side.is_finite() || *side <= 0.0) |
| 703 | { |
| 704 | return Err("Canvas dimensions and scale must be finite and positive".into()); |
| 705 | } |
| 706 | let pixels = size.map(|side| (side * scale).round().max(1.0)); |
| 707 | if pixels |
| 708 | .iter() |
| 709 | .any(|side| f64::from(*side) > f64::from(limit)) |
| 710 | { |
| 711 | return Err("Canvas dimensions exceed the GPU texture limit".into()); |
| 712 | } |
| 713 | Ok(pixels.map(|side| side as u32)) |
| 714 | } |
| 715 | |
| 716 | struct Presentation { |
| 683 | 717 | surface: wgpu::Surface<'static>, |
| 718 | /// Field order keeps the acquired window alive until the surface has dropped. |
| 719 | #[cfg(target_os = "android")] |
| 720 | _window: NativeWindow, |
| 684 | 721 | config: wgpu::SurfaceConfiguration, |
| 685 | | frame: Arc<Frame>, |
| 686 | | section: Arc<Shared>, |
| 687 | | /// A conflict page, which shows what is stored and takes no edits. |
| 688 | | read_only: bool, |
| 689 | | /// The column, in page points, a reading view reflows the page into; it takes no edits. |
| 690 | | reading: Option<f32>, |
| 691 | 722 | } |
| 692 | 723 | |
| 693 | | impl View { |
| 694 | | fn new( |
| 695 | | layer: *mut c_void, |
| 696 | | section: &Section, |
| 697 | | space: ExGuid, |
| 698 | | size: [f32; 2], |
| 699 | | scale: f32, |
| 700 | | ) -> Result<Self> { |
| 701 | | let pixels = size.map(|side| (side * scale).round().max(1.0) as u32); |
| 724 | impl Presentation { |
| 725 | /// # Safety |
| 726 | /// `window` is null or a live platform window; on Apple it outlives the presentation. |
| 727 | unsafe fn new(window: *mut c_void, pixels: [u32; 2]) -> Result<Self> { |
| 728 | let window = std::ptr::NonNull::new(window).ok_or("No native window")?; |
| 729 | #[cfg(target_os = "android")] |
| 730 | let window = { |
| 731 | // SAFETY: the caller lends a live ANativeWindow for this call. |
| 732 | unsafe { ANativeWindow_acquire(window.as_ptr()) }; |
| 733 | NativeWindow(window) |
| 734 | }; |
| 735 | #[cfg(target_os = "android")] |
| 736 | let target = wgpu::SurfaceTargetUnsafe::RawHandle { |
| 737 | raw_display_handle: Some(wgpu::rwh::AndroidDisplayHandle::new().into()), |
| 738 | raw_window_handle: wgpu::rwh::AndroidNdkWindowHandle::new(window.0).into(), |
| 739 | }; |
| 740 | #[cfg(not(target_os = "android"))] |
| 741 | let target = wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(window.as_ptr()); |
| 702 | 742 | let mut gpu = gpu(); |
| 703 | 743 | let instance = match &*gpu { |
| 704 | 744 | Some(gpu) => gpu.instance.clone(), |
| 705 | 745 | None => wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()), |
| 706 | 746 | }; |
| 707 | | // SAFETY: the host passes a live CAMetalLayer that outlives the view. |
| 708 | | let surface = unsafe { |
| 709 | | instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer)) |
| 710 | | }?; |
| 747 | // SAFETY: Android retains its window; the Apple caller keeps its layer alive. |
| 748 | let surface = unsafe { instance.create_surface_unsafe(target) }?; |
| 711 | 749 | if gpu.is_none() { |
| 712 | 750 | let adapter = |
| 713 | 751 | pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { |
| ... | ... | @@ -725,19 +763,71 @@ impl View { |
| 725 | 763 | .ok_or("No supported surface configuration")? |
| 726 | 764 | .format; |
| 727 | 765 | if let Ok(mut renderer) = crash_report::RENDERER.lock() { |
| 728 | | *renderer = format!("Metal ({})", adapter.get_info().name); |
| 766 | let info = adapter.get_info(); |
| 767 | *renderer = format!("{:?} ({})", info.backend, info.name); |
| 729 | 768 | } |
| 730 | 769 | *gpu = Some(Gpu { |
| 731 | 770 | instance, |
| 732 | 771 | renderer: draw::Renderer::new(device, queue, format), |
| 733 | 772 | adapter, |
| 773 | format, |
| 734 | 774 | }); |
| 735 | 775 | } |
| 736 | 776 | let gpu = gpu.as_ref().ok_or("No GPU")?; |
| 737 | | let config = surface |
| 777 | if pixels |
| 778 | .iter() |
| 779 | .any(|side| *side > gpu.renderer.device().limits().max_texture_dimension_2d) |
| 780 | { |
| 781 | return Err("Canvas dimensions exceed the GPU texture limit".into()); |
| 782 | } |
| 783 | if !surface |
| 784 | .get_capabilities(&gpu.adapter) |
| 785 | .formats |
| 786 | .contains(&gpu.format) |
| 787 | { |
| 788 | return Err( |
| 789 | "The native window does not support the shared renderer's texture format".into(), |
| 790 | ); |
| 791 | } |
| 792 | let mut config = surface |
| 738 | 793 | .get_default_config(&gpu.adapter, pixels[0], pixels[1]) |
| 739 | 794 | .ok_or("No supported surface configuration")?; |
| 795 | config.format = gpu.format; |
| 740 | 796 | surface.configure(gpu.renderer.device(), &config); |
| 797 | Ok(Self { |
| 798 | surface, |
| 799 | #[cfg(target_os = "android")] |
| 800 | _window: window, |
| 801 | config, |
| 802 | }) |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | pub struct View { |
| 807 | /// Taken apart on drop, which returns its text engine to `ENGINES`. |
| 808 | canvas: ManuallyDrop<Canvas>, |
| 809 | presentation: Option<Presentation>, |
| 810 | frame: Arc<Frame>, |
| 811 | section: Arc<Shared>, |
| 812 | /// A conflict page, which shows what is stored and takes no edits. |
| 813 | read_only: bool, |
| 814 | /// The column, in page points, a reading view reflows the page into; it takes no edits. |
| 815 | reading: Option<f32>, |
| 816 | } |
| 817 | |
| 818 | impl View { |
| 819 | /// # Safety |
| 820 | /// `window` is null or a live platform window; on Apple it outlives the returned view. |
| 821 | unsafe fn new( |
| 822 | window: *mut c_void, |
| 823 | section: &Section, |
| 824 | space: ExGuid, |
| 825 | size: [f32; 2], |
| 826 | scale: f32, |
| 827 | ) -> Result<Self> { |
| 828 | let pixels = view_pixels(size, scale, u32::MAX)?; |
| 829 | // SAFETY: the caller supplies the platform window's lifetime contract. |
| 830 | let presentation = unsafe { Presentation::new(window, pixels) }?; |
| 741 | 831 | let (mut page, read_only) = section.shared.page(space)?; |
| 742 | 832 | // The page wears its theme: style objects it gives otherwise are restyled first. |
| 743 | 833 | let sheet = section.sheet(page.identity); |
| ... | ... | @@ -758,8 +848,7 @@ impl View { |
| 758 | 848 | canvas.page.editor.styles = sheet; |
| 759 | 849 | Ok(Self { |
| 760 | 850 | canvas: ManuallyDrop::new(canvas), |
| 761 | | surface, |
| 762 | | config, |
| 851 | presentation: Some(presentation), |
| 763 | 852 | frame, |
| 764 | 853 | section: Arc::clone(&section.shared), |
| 765 | 854 | read_only, |
| ... | ... | @@ -768,13 +857,18 @@ impl View { |
| 768 | 857 | } |
| 769 | 858 | |
| 770 | 859 | fn render(&mut self) -> Result<()> { |
| 860 | let Some(presentation) = &mut self.presentation else { |
| 861 | return Ok(()); |
| 862 | }; |
| 771 | 863 | let mut gpu = gpu(); |
| 772 | 864 | let renderer = &mut gpu.as_mut().ok_or("No GPU")?.renderer; |
| 773 | | let frame = match self.surface.get_current_texture() { |
| 865 | let frame = match presentation.surface.get_current_texture() { |
| 774 | 866 | wgpu::CurrentSurfaceTexture::Success(frame) |
| 775 | 867 | | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, |
| 776 | 868 | wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => { |
| 777 | | self.surface.configure(renderer.device(), &self.config); |
| 869 | presentation |
| 870 | .surface |
| 871 | .configure(renderer.device(), &presentation.config); |
| 778 | 872 | return Ok(()); |
| 779 | 873 | } |
| 780 | 874 | _ => return Ok(()), |
| ... | ... | @@ -785,10 +879,13 @@ impl View { |
| 785 | 879 | .page |
| 786 | 880 | .update_pictures(paper, &Waker::from(self.frame.clone())); |
| 787 | 881 | let primitives = canvas.page.primitives(canvas.colors())?; |
| 882 | let target = frame.texture.create_view(&Default::default()); |
| 883 | #[cfg(target_os = "macos")] |
| 884 | let target = target.into(); |
| 788 | 885 | renderer |
| 789 | 886 | .draw( |
| 790 | | &frame.texture.create_view(&Default::default()).into(), |
| 791 | | [self.config.width, self.config.height], |
| 887 | &target, |
| 888 | [presentation.config.width, presentation.config.height], |
| 792 | 889 | paper.color, |
| 793 | 890 | &[canvas.page.viewport.layer(&primitives)], |
| 794 | 891 | ) |
| ... | ... | @@ -853,7 +950,7 @@ impl View { |
| 853 | 950 | }; |
| 854 | 951 | pool_engine(engine); |
| 855 | 952 | self.reading = column.filter(|_| reflowed.is_some()); |
| 856 | | let pixels = [self.config.width, self.config.height]; |
| 953 | let pixels = self.canvas.page.viewport.size; |
| 857 | 954 | let mut canvas = Canvas::new( |
| 858 | 955 | self.canvas.space, |
| 859 | 956 | reflowed.unwrap_or(page), |
| ... | ... | @@ -899,14 +996,18 @@ fn owned(text: String) -> *mut c_char { |
| 899 | 996 | CString::new(text.replace('\0', "")).map_or(std::ptr::null_mut(), CString::into_raw) |
| 900 | 997 | } |
| 901 | 998 | |
| 902 | | /// A view drawing page `id` of `section` into `layer`, a `CAMetalLayer`, `size` points at |
| 903 | | /// `scale` pixels per point; null if it cannot. |
| 999 | /// A view drawing page `id` of `section`, `width` by `height` host points at `scale` |
| 1000 | /// pixels per point; null if it cannot. Android exports this as `sb_view_new_android`. |
| 904 | 1001 | /// |
| 905 | 1002 | /// # Safety |
| 906 | | /// `layer` is a live `CAMetalLayer` that outlives the view; `id` is NUL-terminated UTF-8. |
| 907 | | #[unsafe(no_mangle)] |
| 1003 | /// `window` is null or a live `ANativeWindow` on Android, or a `CAMetalLayer` that outlives the |
| 1004 | /// view on Apple. Android acquires its own reference without consuming the caller's. |
| 1005 | /// The host serializes view calls and detaches Android views before destroying their surfaces. |
| 1006 | /// `id` is NUL-terminated UTF-8. |
| 1007 | #[cfg_attr(target_os = "android", unsafe(export_name = "sb_view_new_android"))] |
| 1008 | #[cfg_attr(not(target_os = "android"), unsafe(no_mangle))] |
| 908 | 1009 | pub unsafe extern "C" fn sb_view_new( |
| 909 | | layer: *mut c_void, |
| 1010 | window: *mut c_void, |
| 910 | 1011 | section: &Section, |
| 911 | 1012 | id: *const c_char, |
| 912 | 1013 | width: f32, |
| ... | ... | @@ -917,11 +1018,40 @@ pub unsafe extern "C" fn sb_view_new( |
| 917 | 1018 | string(id) |
| 918 | 1019 | .parse() |
| 919 | 1020 | .map_err(Into::into) |
| 920 | | .and_then(|space| View::new(layer, section, space, [width, height], scale)), |
| 1021 | .and_then(|space| unsafe { View::new(window, section, space, [width, height], scale) }), |
| 921 | 1022 | ) |
| 922 | 1023 | .map_or(std::ptr::null_mut(), |view| Box::into_raw(Box::new(view))) |
| 923 | 1024 | } |
| 924 | 1025 | |
| 1026 | /// Stops presentation without changing the editor, selection or Undo history. |
| 1027 | #[cfg(any(target_os = "android", test))] |
| 1028 | #[cfg_attr(target_os = "android", unsafe(no_mangle))] |
| 1029 | pub extern "C" fn sb_view_detach_android(view: &mut View) { |
| 1030 | view.presentation = None; |
| 1031 | } |
| 1032 | |
| 1033 | /// Attaches a replacement window at the last size and scale; resize after SurfaceChanged. |
| 1034 | /// Failure leaves a detached editor unchanged; an attached view must be detached first. |
| 1035 | /// |
| 1036 | /// # Safety |
| 1037 | /// `window` is null or a live ANativeWindow. The view acquires its own reference. |
| 1038 | /// The host serializes view calls and detaches before SurfaceDestroyed returns. |
| 1039 | #[cfg(any(target_os = "android", test))] |
| 1040 | #[cfg_attr(target_os = "android", unsafe(no_mangle))] |
| 1041 | pub unsafe extern "C" fn sb_view_attach_android(view: &mut View, window: *mut c_void) -> bool { |
| 1042 | report(if view.presentation.is_some() { |
| 1043 | Err("Detach the current surface before attaching another".into()) |
| 1044 | } else { |
| 1045 | // SAFETY: the caller supplies the platform window's lifetime contract. |
| 1046 | unsafe { Presentation::new(window, view.canvas.page.viewport.size) } |
| 1047 | }) |
| 1048 | .map(|presentation| { |
| 1049 | view.presentation = Some(presentation); |
| 1050 | view.frame.0.store(true, Ordering::Release); |
| 1051 | }) |
| 1052 | .is_some() |
| 1053 | } |
| 1054 | |
| 925 | 1055 | /// # Safety |
| 926 | 1056 | /// `view` came from `sb_view_new` and is not used again. |
| 927 | 1057 | #[unsafe(no_mangle)] |
| ... | ... | @@ -931,17 +1061,27 @@ pub unsafe extern "C" fn sb_view_free(view: *mut View) { |
| 931 | 1061 | |
| 932 | 1062 | #[unsafe(no_mangle)] |
| 933 | 1063 | pub extern "C" fn sb_view_resize(view: &mut View, width: f32, height: f32, scale: f32) { |
| 934 | | let pixels = [width, height].map(|side| (side * scale).round().max(1.0) as u32); |
| 1064 | let gpu = gpu(); |
| 1065 | let Some(gpu) = gpu.as_ref() else { return }; |
| 1066 | let Some(pixels) = report(view_pixels( |
| 1067 | [width, height], |
| 1068 | scale, |
| 1069 | gpu.renderer.device().limits().max_texture_dimension_2d, |
| 1070 | )) else { |
| 1071 | return; |
| 1072 | }; |
| 935 | 1073 | let canvas = &mut view.canvas; |
| 936 | 1074 | if scale != canvas.scale { |
| 937 | 1075 | canvas.scale = scale; |
| 938 | 1076 | let _ = report(canvas.page.scale_factor_changed(scale)); |
| 939 | 1077 | } |
| 940 | 1078 | let _ = report(canvas.page.resized(pixels)); |
| 941 | | view.config.width = pixels[0]; |
| 942 | | view.config.height = pixels[1]; |
| 943 | | if let Some(gpu) = &*gpu() { |
| 944 | | view.surface.configure(gpu.renderer.device(), &view.config); |
| 1079 | if let Some(presentation) = &mut view.presentation { |
| 1080 | presentation.config.width = pixels[0]; |
| 1081 | presentation.config.height = pixels[1]; |
| 1082 | presentation |
| 1083 | .surface |
| 1084 | .configure(gpu.renderer.device(), &presentation.config); |
| 945 | 1085 | } |
| 946 | 1086 | } |
| 947 | 1087 | |