diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 38a8211b87e5..21c56b864f03 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -15,7 +15,10 @@ libloading = { version = "0.8.5", optional = true } im-rc = { version = "15", optional = true } async-channel = { version = "2.3", optional = true } tokio = { version = "1", features = ["full"], optional = true } -reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false, optional = true } +reqwest = { version = "0.12", features = [ + "json", + "rustls-tls", +], default-features = false, optional = true } serde = { version = "1.0", features = ["derive"], optional = true } gtk.workspace = true @@ -36,12 +39,18 @@ v4_6 = ["gtk/v4_6"] v4_10 = ["gtk/v4_10"] v4_12 = ["gtk/v4_12"] v4_14 = ["gtk/v4_14"] +v4_16 = ["gtk/v4_16"] [[bin]] name = "about_dialog" path = "about_dialog/main.rs" required-features = ["v4_6"] +[[bin]] +name = "accessible_text" +path = "accessible_text/main.rs" +required-features = ["v4_16"] + [[bin]] name = "basics" path = "basics/main.rs" diff --git a/examples/README.md b/examples/README.md index 2d74d0878ff8..57b3c32e74e6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,11 +15,15 @@ cargo run --bin basics ``` - [Basic example](./basics/) +- [About Dialog](./about_dialog/) +- [Implementing the Accessible Text Interface](./accessible_text/) - [Using the Builder pattern](./builder_pattern/) - [Clipboard](./clipboard/) - [Clock example](./clock/) - [Column View Datagrid Example](./column_view_datagrid/) +- [Composite Dialog](./composite_dialog/) - [Composite Template](./composite_template/) +- [Confetti Snapshot Animation](./confetti_snapshot_animation/) - [Content Provider](./content_provider/) - [CSS](./css/) - [Custom Application](./custom_application/) @@ -36,10 +40,13 @@ cargo run --bin basics - [FemtoVG Area](./femtovg_area/) - [Fill and Stroke](./fill_and_stroke/) - [FlowBox](./flow_box/) +- [GIF Paintable](./gif_paintable/) - [Glium GL-Area](./glium_gl_area/) - [Grid Packing](./grid_packing) -- [GtkBuilder example](./gtk_builder/) +- [GtkBuilder](./gtk_builder/) +- [ListBox and ListModel](./list_box_model/) - [ListView: Applications Launcher](./list_view_apps_launcher/) +- [Menubar](./menubar/) - [Rotation Bin](./rotation_bin/) - [Scale](./scale/) - [Scale Bin](./scale_bin/) diff --git a/examples/accessible_text/README.md b/examples/accessible_text/README.md new file mode 100644 index 000000000000..846fb45ed10e --- /dev/null +++ b/examples/accessible_text/README.md @@ -0,0 +1,7 @@ +# Implementing the Accessible Text Interface + +This example creates a custom text editing widget that implements the Accessible Text Interface. This interface is used for providing screen reader accessibility to custom widgets. + +To test this implementation, enable the screen reader (Orca is enabled via Super+Alt+S) and edit / navigate the text. + +The widget mostly just delegates its implementation to the parent `TextView`. diff --git a/examples/accessible_text/main.rs b/examples/accessible_text/main.rs new file mode 100644 index 000000000000..05212ace0b5a --- /dev/null +++ b/examples/accessible_text/main.rs @@ -0,0 +1,24 @@ +mod text_view; + +use gtk::{glib, prelude::*}; +use text_view::AccessibleTextView; + +fn main() -> glib::ExitCode { + let application = gtk::Application::builder() + .application_id("com.github.gtk-rs.examples.accessible_text") + .build(); + application.connect_activate(build_ui); + application.run() +} + +fn build_ui(application: >k::Application) { + let window = gtk::ApplicationWindow::new(application); + + window.set_title(Some("Accessible Text Example")); + window.set_default_size(260, 140); + + let text_view = glib::Object::new::(); + window.set_child(Some(&text_view)); + + window.present(); +} diff --git a/examples/accessible_text/text_view.rs b/examples/accessible_text/text_view.rs new file mode 100644 index 000000000000..98ca5ed5a3a8 --- /dev/null +++ b/examples/accessible_text/text_view.rs @@ -0,0 +1,102 @@ +use gtk::glib; +use gtk::subclass::prelude::*; + +mod imp { + use gtk::{graphene, ACCESSIBLE_ATTRIBUTE_OVERLINE, ACCESSIBLE_ATTRIBUTE_OVERLINE_SINGLE}; + + use super::*; + + #[derive(Default)] + pub struct AccessibleTextView {} + + #[glib::object_subclass] + impl ObjectSubclass for AccessibleTextView { + const NAME: &'static str = "AccessibleTextView"; + type Type = super::AccessibleTextView; + type ParentType = gtk::TextView; + type Interfaces = (gtk::AccessibleText,); + } + + impl ObjectImpl for AccessibleTextView {} + impl WidgetImpl for AccessibleTextView {} + impl AccessibleTextImpl for AccessibleTextView { + fn attributes( + &self, + offset: u32, + ) -> Vec<(gtk::AccessibleTextRange, glib::GString, glib::GString)> { + let attributes = self.parent_attributes(offset); + println!("attributes({offset}) -> {attributes:?}"); + attributes + } + + fn caret_position(&self) -> u32 { + let pos = self.parent_caret_position(); + println!("caret_position() -> {pos}"); + pos + } + + fn contents(&self, start: u32, end: u32) -> Option { + let content = self.parent_contents(start, end); + println!( + "contents({start}, {end}) -> {:?}", + content + .as_ref() + .map(|c| std::str::from_utf8(c.as_ref()).unwrap()) + ); + content + } + + fn contents_at( + &self, + offset: u32, + granularity: gtk::AccessibleTextGranularity, + ) -> Option<(u32, u32, glib::Bytes)> { + let contents = self.parent_contents_at(offset, granularity); + println!( + "contents_at offset({offset}, {granularity:?}) -> {:?}", + contents + .as_ref() + .map(|(s, e, c)| (s, e, std::str::from_utf8(c.as_ref()).unwrap())) + ); + contents + } + + fn default_attributes(&self) -> Vec<(glib::GString, glib::GString)> { + let mut attrs = self.parent_default_attributes(); + + // Attributes can be added and removed + attrs.push(( + ACCESSIBLE_ATTRIBUTE_OVERLINE.to_owned(), + ACCESSIBLE_ATTRIBUTE_OVERLINE_SINGLE.to_owned(), + )); + println!("default_attributes() -> {attrs:?}"); + attrs + } + + fn selection(&self) -> Vec { + let selection = self.parent_selection(); + println!("selection() -> {selection:?}"); + selection + } + + fn extents(&self, start: u32, end: u32) -> Option { + let extents = self.parent_extents(start, end); + println!("extents({start}, {end}) -> {extents:?}"); + extents + } + + fn offset(&self, point: &graphene::Point) -> Option { + let offset = self.parent_offset(point); + println!("offset({:?}) -> {offset:?}", point); + offset + } + } + + impl TextViewImpl for AccessibleTextView {} +} + +glib::wrapper! { + pub struct AccessibleTextView(ObjectSubclass) + @extends gtk::Widget, gtk::TextView, + @implements gtk::Accessible, gtk::AccessibleText, gtk::Buildable, gtk::ConstraintTarget, gtk::Scrollable; +} diff --git a/gtk4/src/accessible_text_range.rs b/gtk4/src/accessible_text_range.rs index 10a7a3072b0b..7159e17e3c01 100644 --- a/gtk4/src/accessible_text_range.rs +++ b/gtk4/src/accessible_text_range.rs @@ -1,17 +1,33 @@ // Take a look at the license at the top of the repository in the LICENSE file. -#[derive(Copy, Clone)] -#[doc(alias = "GtkAccessibleTextRange")] -#[repr(transparent)] -pub struct AccessibleTextRange(crate::ffi::GtkAccessibleTextRange); +use crate::ffi; +use glib::translate::*; + +glib::wrapper! { + #[doc(alias = "GtkAccessibleTextRange")] + pub struct AccessibleTextRange(BoxedInline); +} impl AccessibleTextRange { + pub fn new(start: usize, length: usize) -> Self { + skip_assert_initialized!(); + unsafe { AccessibleTextRange::unsafe_from(ffi::GtkAccessibleTextRange { start, length }) } + } + pub fn start(&self) -> usize { - self.0.start + self.inner.start + } + + pub fn set_start(&mut self, start: usize) { + self.inner.start = start; } pub fn length(&self) -> usize { - self.0.length + self.inner.length + } + + pub fn set_length(&mut self, length: usize) { + self.inner.length = length } } diff --git a/gtk4/src/lib.rs b/gtk4/src/lib.rs index 60536d7a5432..4fa96f711e04 100644 --- a/gtk4/src/lib.rs +++ b/gtk4/src/lib.rs @@ -199,6 +199,9 @@ mod tree_view; mod tree_view_column; mod widget; +#[cfg(feature = "v4_14")] +#[cfg_attr(docsrs, doc(cfg(feature = "v4_14")))] +pub use accessible_text_range::AccessibleTextRange; pub use bitset_iter::BitsetIter; pub use border::Border; pub use builder_cscope::BuilderCScope; diff --git a/gtk4/src/subclass/accessible_text.rs b/gtk4/src/subclass/accessible_text.rs new file mode 100644 index 000000000000..2a8351a1d14b --- /dev/null +++ b/gtk4/src/subclass/accessible_text.rs @@ -0,0 +1,688 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +// rustdoc-stripper-ignore-next +//! Traits intended for implementing the [`AccessibleText`] interface. + +use crate::{ + ffi, subclass::prelude::*, AccessibleText, AccessibleTextGranularity, AccessibleTextRange, +}; +use glib::object::Cast; +use glib::{translate::*, GString}; + +pub trait AccessibleTextImpl: WidgetImpl { + #[doc(alias = "get_attributes")] + fn attributes(&self, offset: u32) -> Vec<(AccessibleTextRange, GString, GString)> { + self.parent_attributes(offset) + } + + #[doc(alias = "get_caret_position")] + fn caret_position(&self) -> u32 { + self.parent_caret_position() + } + + #[doc(alias = "get_contents")] + fn contents(&self, start: u32, end: u32) -> Option { + self.parent_contents(start, end) + } + + #[doc(alias = "get_contents_at")] + fn contents_at( + &self, + offset: u32, + granularity: crate::AccessibleTextGranularity, + ) -> Option<(u32, u32, glib::Bytes)> { + self.parent_contents_at(offset, granularity) + } + + #[doc(alias = "get_default_attributes")] + fn default_attributes(&self) -> Vec<(GString, GString)> { + self.parent_default_attributes() + } + + #[cfg(feature = "v4_16")] + #[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] + #[doc(alias = "get_extents")] + fn extents(&self, start: u32, end: u32) -> Option { + self.parent_extents(start, end) + } + + #[cfg(feature = "v4_16")] + #[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] + #[doc(alias = "get_offset")] + fn offset(&self, point: &graphene::Point) -> Option { + self.parent_offset(point) + } + + #[doc(alias = "get_selection")] + fn selection(&self) -> Vec { + self.parent_selection() + } +} + +pub trait AccessibleTextImplExt: AccessibleTextImpl { + fn parent_attributes(&self, offset: u32) -> Vec<(AccessibleTextRange, GString, GString)> { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_attributes + .expect("no parent \"get_attributes\" implementation"); + + let mut n_ranges = std::mem::MaybeUninit::uninit(); + let mut ranges = std::ptr::null_mut(); + let mut attribute_names = std::ptr::null_mut(); + let mut attribute_values = std::ptr::null_mut(); + + let is_set: bool = from_glib(func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + offset, + n_ranges.as_mut_ptr(), + &mut ranges, + &mut attribute_names, + &mut attribute_values, + )); + + if !is_set + || n_ranges.assume_init() == 0 + || ranges.is_null() + || attribute_names.is_null() + || attribute_values.is_null() + { + Vec::new() + } else { + let mut names = glib::StrV::from_glib_full(attribute_names).into_iter(); + let mut values = glib::StrV::from_glib_full(attribute_values).into_iter(); + + glib::Slice::from_glib_container_num(ranges, n_ranges.assume_init()) + .into_iter() + .flat_map(|range| { + if let (Some(name), Some(value)) = (names.next(), values.next()) { + Some((range, name, value)) + } else { + None + } + }) + .collect() + } + } + } + + fn parent_caret_position(&self) -> u32 { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_caret_position + .expect("no parent \"get_caret_position\" implementation"); + + func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + ) + } + } + + fn parent_contents(&self, start: u32, end: u32) -> Option { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface).get_contents?; + + from_glib_full(func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + start, + end, + )) + } + } + + fn parent_contents_at( + &self, + offset: u32, + granularity: crate::AccessibleTextGranularity, + ) -> Option<(u32, u32, glib::Bytes)> { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface).get_contents_at?; + + let mut start = std::mem::MaybeUninit::uninit(); + let mut end = std::mem::MaybeUninit::uninit(); + + let bytes = func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + offset, + granularity.into_glib(), + start.as_mut_ptr(), + end.as_mut_ptr(), + ); + + if !bytes.is_null() { + Some(( + start.assume_init(), + end.assume_init(), + from_glib_full(bytes), + )) + } else { + None + } + } + } + + fn parent_default_attributes(&self) -> Vec<(GString, GString)> { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_default_attributes + .expect("no parent \"get_default_attributes\" implementation"); + + let mut attribute_names = std::ptr::null_mut(); + let mut attribute_values = std::ptr::null_mut(); + + func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + &mut attribute_names, + &mut attribute_values, + ); + + if attribute_names.is_null() || attribute_values.is_null() { + Vec::new() + } else { + glib::StrV::from_glib_full(attribute_names) + .into_iter() + .zip(glib::StrV::from_glib_full(attribute_values)) + .collect() + } + } + } + + #[cfg(feature = "v4_16")] + #[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] + fn parent_extents(&self, start: u32, end: u32) -> Option { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_extents + .expect("no parent \"get_extents\" implementation"); + + let mut extents = std::mem::MaybeUninit::uninit(); + + let filled = from_glib(func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + start, + end, + extents.as_mut_ptr(), + )); + + if filled { + Some(graphene::Rect::unsafe_from(extents.assume_init())) + } else { + None + } + } + } + + #[cfg(feature = "v4_16")] + #[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] + fn parent_offset(&self, point: &graphene::Point) -> Option { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_offset + .expect("no parent \"get_offset\" implementation"); + + let mut offset = std::mem::MaybeUninit::uninit(); + + let offset_set = from_glib(func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + point.to_glib_none().0, + offset.as_mut_ptr(), + )); + + if offset_set { + Some(offset.assume_init()) + } else { + None + } + } + } + + fn parent_selection(&self) -> Vec { + unsafe { + let type_data = Self::type_data(); + let parent_iface = type_data.as_ref().parent_interface::() + as *const ffi::GtkAccessibleTextInterface; + + let func = (*parent_iface) + .get_selection + .expect("no parent \"get_selection\" implementation"); + + let mut n_ranges = std::mem::MaybeUninit::uninit(); + let mut ranges = std::ptr::null_mut(); + + let valid = from_glib(func( + self.obj() + .unsafe_cast_ref::() + .to_glib_none() + .0, + n_ranges.as_mut_ptr(), + &mut ranges, + )); + + if valid { + let n = n_ranges.assume_init(); + AccessibleTextRange::from_glib_container_num_as_vec(ranges, n) + } else { + Vec::new() + } + } + } +} + +impl AccessibleTextImplExt for T {} + +unsafe impl IsImplementable for AccessibleText { + fn interface_init(iface: &mut glib::Interface) { + let iface = iface.as_mut(); + + iface.get_contents = Some(accessible_text_get_contents::); + iface.get_contents_at = Some(accessible_text_get_contents_at::); + iface.get_caret_position = Some(accessible_text_get_caret_position::); + iface.get_selection = Some(accessible_text_get_selection::); + iface.get_attributes = Some(accessible_text_get_attributes::); + iface.get_default_attributes = Some(accessible_text_get_default_attributes::); + + #[cfg(feature = "v4_16")] + { + iface.get_extents = Some(accessible_text_get_extents::); + iface.get_offset = Some(accessible_text_get_offset::); + } + } +} + +unsafe extern "C" fn accessible_text_get_contents( + accessible_text: *mut ffi::GtkAccessibleText, + start: u32, + end: u32, +) -> *mut glib::ffi::GBytes { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let contents = imp.contents(start, end); + contents.into_glib_ptr() +} + +unsafe extern "C" fn accessible_text_get_contents_at( + accessible_text: *mut ffi::GtkAccessibleText, + offset: libc::c_uint, + granularity: ffi::GtkAccessibleTextGranularity, + start: *mut libc::c_uint, + end: *mut libc::c_uint, +) -> *mut glib::ffi::GBytes { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + if let Some((r_start, r_end, bytes)) = + imp.contents_at(offset, AccessibleTextGranularity::from_glib(granularity)) + { + if !start.is_null() { + *start = r_start; + } + if !end.is_null() { + *end = r_end; + } + + bytes.into_glib_ptr() + } else { + std::ptr::null_mut() + } +} + +unsafe extern "C" fn accessible_text_get_caret_position( + accessible_text: *mut ffi::GtkAccessibleText, +) -> u32 { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + imp.caret_position() +} + +unsafe extern "C" fn accessible_text_get_selection( + accessible_text: *mut ffi::GtkAccessibleText, + n_ranges: *mut libc::size_t, + ranges: *mut *mut ffi::GtkAccessibleTextRange, +) -> glib::ffi::gboolean { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let r_ranges = imp.selection(); + let n: usize = r_ranges.len(); + *n_ranges = n; + + if n == 0 { + false + } else { + *ranges = r_ranges.to_glib_container().0; + + true + } + .into_glib() +} + +unsafe extern "C" fn accessible_text_get_attributes( + accessible_text: *mut ffi::GtkAccessibleText, + offset: u32, + n_ranges: *mut libc::size_t, + ranges: *mut *mut ffi::GtkAccessibleTextRange, + attribute_names: *mut *mut *mut libc::c_char, + attribute_values: *mut *mut *mut libc::c_char, +) -> glib::ffi::gboolean { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let attrs = imp.attributes(offset); + let n: usize = attrs.len(); + *n_ranges = n; + + if n == 0 { + *attribute_names = std::ptr::null_mut(); + *attribute_values = std::ptr::null_mut(); + + false + } else { + let mut c_ranges = glib::Slice::with_capacity(attrs.len()); + let mut c_names = glib::StrV::with_capacity(attrs.len()); + let mut c_values = glib::StrV::with_capacity(attrs.len()); + + for (range, name, value) in attrs { + c_ranges.push(range); + c_names.push(name); + c_values.push(value); + } + + *ranges = c_ranges.to_glib_container().0; + *attribute_names = c_names.into_glib_ptr(); + *attribute_values = c_values.into_glib_ptr(); + + true + } + .into_glib() +} + +unsafe extern "C" fn accessible_text_get_default_attributes( + accessible_text: *mut ffi::GtkAccessibleText, + attribute_names: *mut *mut *mut libc::c_char, + attribute_values: *mut *mut *mut libc::c_char, +) { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let attrs = imp.default_attributes(); + + if attrs.is_empty() { + *attribute_names = std::ptr::null_mut(); + *attribute_values = std::ptr::null_mut(); + } else { + let mut c_names = glib::StrV::with_capacity(attrs.len()); + let mut c_values = glib::StrV::with_capacity(attrs.len()); + + for (name, value) in attrs { + c_names.push(name); + c_values.push(value); + } + + *attribute_names = c_names.into_glib_ptr(); + *attribute_values = c_values.into_glib_ptr(); + } +} + +#[cfg(feature = "v4_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] +unsafe extern "C" fn accessible_text_get_extents( + accessible_text: *mut ffi::GtkAccessibleText, + start: u32, + end: u32, + extents: *mut graphene::ffi::graphene_rect_t, +) -> glib::ffi::gboolean { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let rect = imp.extents(start, end); + + if let Some(rect) = rect { + *extents = *rect.as_ptr(); + + true + } else { + false + } + .into_glib() +} + +#[cfg(feature = "v4_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v4_16")))] +unsafe extern "C" fn accessible_text_get_offset( + accessible_text: *mut ffi::GtkAccessibleText, + point: *const graphene::ffi::graphene_point_t, + offset: *mut libc::c_uint, +) -> glib::ffi::gboolean { + let instance = &*(accessible_text as *mut T::Instance); + let imp = instance.imp(); + + let pos = imp.offset(&from_glib_borrow(point)); + + if let Some(pos) = pos { + if !offset.is_null() { + *offset = pos; + } + true + } else { + false + } + .into_glib() +} + +#[cfg(test)] +mod test { + use crate as gtk4; + use crate::prelude::*; + use crate::subclass::prelude::*; + + mod imp { + use super::*; + + #[derive(Default)] + pub struct TestTextView {} + + #[glib::object_subclass] + impl ObjectSubclass for TestTextView { + const NAME: &'static str = "TestTextView"; + type Type = super::TestTextView; + type ParentType = crate::TextView; + type Interfaces = (crate::AccessibleText,); + } + + impl ObjectImpl for TestTextView {} + impl WidgetImpl for TestTextView {} + impl AccessibleTextImpl for TestTextView { + fn attributes( + &self, + offset: u32, + ) -> Vec<( + crate::accessible_text_range::AccessibleTextRange, + glib::GString, + glib::GString, + )> { + self.parent_attributes(offset) + } + + fn caret_position(&self) -> u32 { + self.parent_caret_position() + } + + fn contents(&self, start: u32, end: u32) -> Option { + self.parent_contents(start, end) + } + + fn contents_at( + &self, + offset: u32, + granularity: crate::AccessibleTextGranularity, + ) -> Option<(u32, u32, glib::Bytes)> { + self.parent_contents_at(offset, granularity) + } + + fn default_attributes(&self) -> Vec<(glib::GString, glib::GString)> { + self.parent_default_attributes() + } + + fn selection(&self) -> Vec { + self.parent_selection() + } + + #[cfg(feature = "v4_16")] + fn extents(&self, start: u32, end: u32) -> Option { + self.parent_extents(start, end) + } + + #[cfg(feature = "v4_16")] + fn offset(&self, point: &graphene::Point) -> Option { + self.parent_offset(point) + } + } + + impl TextViewImpl for TestTextView {} + impl TestTextView {} + } + + glib::wrapper! { + pub struct TestTextView(ObjectSubclass) + @extends crate::Widget, crate::TextView, + @implements crate::Accessible, crate::AccessibleText, crate::Buildable, crate::ConstraintTarget, crate::Scrollable; + } + + impl TestTextView {} + + #[crate::test] + fn test_accessible_text_iface() { + let text: TestTextView = glib::Object::new(); + let mut iter = text.buffer().iter_at_offset(0); + text.buffer() + .insert_markup(&mut iter, "Lorem Ipsum dolor sit. amnet"); + + let (range, _, value) = text + .imp() + .attributes(0) + .into_iter() + .find(|(_, name, _)| name == "weight") + .unwrap(); + + assert_eq!(range.start(), 0); + assert_eq!(range.length(), "Lorem Ipsum".len()); + assert_eq!(value, "700"); + + assert_eq!( + text.imp().caret_position(), + "Lorem Ipsum dolor sit. amnet".len() as u32 + ); + let pos = "Lorem Ipsum ".len(); + let iter = text.buffer().iter_at_offset(pos as i32); + text.buffer().place_cursor(&iter); + assert_eq!(text.imp().caret_position(), pos as u32); + + assert_eq!( + std::str::from_utf8( + &text + .imp() + .contents_at(pos as u32, crate::AccessibleTextGranularity::Character) + .unwrap() + .2 + ) + .unwrap(), + "d" + ); + assert_eq!( + std::str::from_utf8( + &text + .imp() + .contents_at(pos as u32, crate::AccessibleTextGranularity::Word) + .unwrap() + .2 + ) + .unwrap(), + "dolor " + ); + assert_eq!( + std::str::from_utf8( + &text + .imp() + .contents_at(pos as u32, crate::AccessibleTextGranularity::Line) + .unwrap() + .2 + ) + .unwrap(), + "Lorem Ipsum dolor sit. amnet" + ); + + assert_eq!( + "Lorem Ipsum\0", + std::str::from_utf8(&text.imp().contents(0, 11).unwrap()).unwrap() + ); + + assert!(text + .imp() + .default_attributes() + .iter() + .any(|(name, value)| name == "editable" && value == "true")); + text.buffer().select_range( + &text.buffer().iter_at_offset(0), + &text.buffer().iter_at_offset(10), + ); + let selected_range = text.imp().selection()[0]; + assert_eq!(selected_range.start(), 0); + assert_eq!(selected_range.length(), 10); + + #[cfg(feature = "v4_16")] + { + let _extents = text.imp().extents(0, 20); + let _offset = text.imp().offset(&graphene::Point::new(10.0, 10.0)); + } + } +} diff --git a/gtk4/src/subclass/mod.rs b/gtk4/src/subclass/mod.rs index 2acf05ff3294..dc226afec933 100644 --- a/gtk4/src/subclass/mod.rs +++ b/gtk4/src/subclass/mod.rs @@ -20,6 +20,9 @@ pub mod accessible; #[cfg(feature = "v4_10")] #[cfg_attr(docsrs, doc(cfg(feature = "v4_10")))] pub mod accessible_range; +#[cfg(feature = "v4_14")] +#[cfg_attr(docsrs, doc(cfg(feature = "v4_14")))] +pub mod accessible_text; pub mod actionable; pub mod adjustment; pub mod application; @@ -98,6 +101,9 @@ pub mod prelude { #[cfg(feature = "v4_10")] #[cfg_attr(docsrs, doc(cfg(feature = "v4_10")))] pub use super::accessible_range::{AccessibleRangeImpl, AccessibleRangeImplExt}; + #[cfg(feature = "v4_14")] + #[cfg_attr(docsrs, doc(cfg(feature = "v4_14")))] + pub use super::accessible_text::{AccessibleTextImpl, AccessibleTextImplExt}; #[cfg(feature = "v4_12")] #[cfg_attr(docsrs, doc(cfg(feature = "v4_12")))] pub use super::section_model::{SectionModelImpl, SectionModelImplExt};